formations/python-avancé/python-avancé.md

24 KiB

Python avancé

Présenté par

Julien Palard julien@palard.fr

https://mdk.fr

Notes:

En initiation, on utilise (le for par exemple), en avancé on crée (des itérables par exemple).

  • Exemples concrets et définitions abstraites
    • Pas de class Foo, le cerveau ne peut s'accrocher à rien.
    • Le bonheur est dans le chemin et dans la finalité
  • Contenu différenciant
    • Pas de class Foo, tout le monde le fait déjà.
    • Détaillez toutes les étapes, même les plus petites.
    • Soyez drôle ! Donnez envie !

J'ai 5 jours, donc ~200 slides.

« Tout est objet »

Comme en Java, #oupas

Notes:

  • Sortir un interpréteur.
  • Leur faire essayer de deviner ce qui pourrait ne pas être une classe.
  • Démo avec:
  • un nombre entier, #obvious, c'est géré par Python
    • ouvrir une parenthèse si nécessaire, avec 6 ** 6 ** 6
  • un float, en les faisant hésiter vu qu'ils sont « gérés par le CPU »
  • une fonction
  • une classe (et une instance)
  • range !
  • module !!

OK mais pas for, def, ... ce sont des mots clefs.

Donc, tout a des attributs…

Notes:

Exercice : avec des set, et dir(), trouver la liste des attributs communs à une fonction, disons max et à un int, disons 42, combien y'en-a-il ? Moi 23. Combien object en a-il ?

Même un int ?

>>> (42).__bool__() is bool(42)
True

Ou help(42 .to_bytes).

Notes:

Ouvrir une parenthèse sur la notion de vérité, ce qui est :

  • Vide
  • Égal à zéro
  • None ou False

c'est faux, le reste, c'est vrai.

Les noms

Notes:

Faire le schéma à deux colonnes: noms → mémoire. https://dreampuf.github.io/GraphvizOnline/#%23%20import%20math%0A%0A%23%20def%20print_tau()%3A%0A%23%20%20%20...%0A%0A%0Adigraph%20G%20%7B%0A%0A%20%20subgraph%20cluster_0%20%7B%0A%20%20%20%20%20label%20%3D%20%22Noms%22%3B%0A%20%20%20%20%20math%3B%0A%20%20%20%20%20print_tau%3B%0A%20%20%7D%0A%0A%20%20subgraph%20cluster_1%20%7B%0A%20%20%20%20%20label%20%3D%20%22Objets%22%3B%0A%20%20%20%20%20%22%3Cmodule%20math%3E%22%0A%20%20%20%20%20%22%3Cfunction%20print_tau%3E%22%0A%20%20%7D%0A%20%20%0A%20%20math%20-%3E%20%22%3Cmodule%20math%3E%22%0A%20%20print_tau%20-%3E%20%22%3Cfunction%20print_tau%3E%22%0A%7D

En Python avancé bien insister sur le fait qu'un objet en mémoire à une adresse.

Insister sur le fait qu'un paramètre de fonction n'est qu'un nom. On a donc pas de « passage par valeur » chez nous.

Bien préciser qu'on ne peut pas « délier » un nom pour le faire pointer sur rien (en ce cas on le fait pointer sur None).

J'ai 5mn pour vous parler de for

Notes:

Déjà, c'est pas un objet.

Jusqu'où peut-on creuser ?

for itère des itérables

  • Itérable,
  • itérateur.

Notes:

On peut très bien imaginer un itérateur capable d'itérer un itérable, mais aussi une séquence, une collections, ...

Le protocole « séquence »

Implémente __getitem__ et __len__.

Notes:

Exercice, implémenter un range(), mais sans stop ni step.

Petite parenthèse : range, c'est une classe ou une fonction ?

Le protocole d'itération

  • iter() : Crée un itérateur à partir d'un itérable.
  • next() : Demande l'élément suivant à un itérateur.
  • raise StopIteration : C'est terminé.

Notes:

Première démo REPL sur une liste « on reste utilisateurs de Python ».

Le protocole d'itération

__iter__ et __next__

Notes:

Démo REPL sur une liste « on perçoit comment on va pouvoir l'implémenter ».

La différence ? Petite parenthèse : iter() peut utiliser soit le protocole séquence soit le protocole d'itération, et fait quelques vérifications (que l'itérateur renvoyé soit bien un itérateur).

Duck typing

>>> class Counter:
...     def __getitem__(self, i):
...         return i
...
>>> i = iter(Counter())
>>> i
<iterator object at ...>
>>> next(i)
0
>>> next(i)
1
>>> next(i)
2

Notes:

Via le protocole séquence, __len__ n'est pas utilisé donc ça se passe bien.

Ne pas confondre

Itérateur :

class CounterIterator:
    def __init__(self):
        self.i = -1

    def __iter__(self):
        return self

    def __next__(self):
        self.i += 1
        return self.i

Ne pas confondre

>>> c = CounterIterator()
>>> for i in c:
...     print(i)
...     if i >= 2: break
...
0
1
2
>>> for i in c:
...     print(i)
...     if i >= 2: break
...
3

Ne pas confondre

Et itérable :

class CounterIterable:
    def __iter__(self):
        return CounterIterator()

Ne pas confondre

>>> c = CounterIterable()
>>> for i in c:
...     print(i)
...     if i >= 2: break
...
0
1
2
>>> for i in c:
...     print(i)
...     if i >= 2: break
...
0
1
2

Peut-on faire plus simple ?

class GenCounter:
    def __iter__(self):
        i = 0
        while True:
            yield i
            i += 1

Pendant qu'on parle de for

Connaissez-vous le else du for ?

Notes:

Il ne s'exécute que si le for sort sans break.

else

>>> n = 13
>>> for i in range(2, n - 1):
...     if n % i == 0:
...         print(f"{n} is not prime")
...         break
... else:
...     print(f"{n} is prime")
13 is prime

Notes:

Typiquement utile lors des recherches, la sémantique :

  • Trouvé, plus besoin de chercher, break.
  • else: pas trouvé.

Fonctionne aussi sur le while.

On parlais d'itérables

Si on parlais d'unpacking ?

Notes:

Pour se remémorer ces choses, cherchez les PEPs, typiquement la 448, la 3132, ...

  • Parler de deep unpacking.
  • Parler de head, *rest, ...

Les objets

Rappels

  • Keep it simple.
  • Flat is better than nested.

classmethod, staticmethod

La MRO

Notes:

Simple démo REPL : bool.__mro__.

super() !

Notes:

Et la coopération, démo avec deux classes : TCPConnection qui prend host, port, timeout, et HTTPConnection qui prend url, method, ...`

Démo aussi : passer un argument de trop et voir que object() se plains.

Antisèche : https://wyz.fr/3Z8

Le protocole « descripteur »

def __get__(self, instance, owner=None): ...
def __set__(self, instance, value): ...

Notes:

Et __delete__ et __set_name__.

  • instance... c'est l'instance.
  • owner, c'est le type, il est toujours connu donc "devrait" toujours être donné
  • Si instance n'est pas donnée, c'est qu'on accède à l'attribut sur le type.

Exercice : https://www.hackinscience.org/exercises/temperature-class

Métaclasses

Puisqu'une classe est un objet, une métaclasse c'est le type du type.

Notes:

En initiation on dit "ça ne vous servira pas". En avancé on dit __init_subclass__ couvrira tous vos besoins.

Métaclasse

  • __new__ et __init__ d'une classe servent à personaliser l'instance.
  • __new__ et __init__ d'une metaclasse servent à personaliser une classe.

Notes:

Vous pouvez aussi utiliser un décorateur pour personaliser une classe.

Langage

IEEE 754

f"http://{.1 + .2}.com"

Notes:

Notez ! Et au besoin utilisez le module Decimal.

Définir vos exceptions

Il suffit d'hériter d'Exception, rien de plus.

>>> class DBError(Exception): pass
...
>>> raise DBError("No such entry")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
__main__.DBError: No such entry

Notes:

library/exceptions.html → hierarchy

try, except, else, finally

Les gestionnaires de contexte

with open("/etc/hosts") as f:
    f.read()

Notes:

En initiation on apprend a les utiliser. En avancé on apprend à en faire.

Les gestionnaires de contexte

def __enter__(self): ...
def __exit__(self, exc_type, exc_value, tb): ...

Notes:

Expliquer le protocole.

Les gestionnaires de contexte

class transaction:
    def __init__(self, db):
        self.db = db
    def __enter__(self):
        self.db.begin()
    def __exit__(self, type, value, tb):
        if type is None:
            self.db.commit()
        else:
            self.db.rollback()

Notes:

C'est un exemple de gestionnaire de contexte de transaction de base de donnée.

Astuce, __enter__ peut renvoyer un tuple, qu'on peut décomposer à droite du as, typiquement ifile, ofile.

Les décorateurs

@

Notes:

En initiation on apprend a les utiliser. En avancé on apprend à en faire.

Just for doctest:

def clock(f=None, *args, **kwargs):
    return lambda *args: None

Les décorateurs

@clock
def fib(n):
    ...

équivaut à

fib = clock(fib)

Notes:

Bien insister sur le fait que @ est bien séparé de son dotted_name, pas n'importe quelle expression. sur le fait qu'on peut les empiler (clarifier l'ordre).

Les décorateurs

@clock(deadline=10)
def fib(n):
    ...

équivaut à

fib = clock(deadline=10)(fib)

Notes:

Rappeler que () n'est qu'un opérateur.

Les décorateurs

Faire ses propres décorateurs.

Notes:

Leur faire implémenter un décorateur @clock.

def clock(func):
    def clocked(*args):
        t0 = time.perf_counter()
        result = func(*args)
        elapsed = time.perf_counter() -t0
        name = func.__name__
        arg_str = ', '.join(repr(arg) for arg in args)
        print(f"[{elapsed:.08f}s] {name}({arg_str}) -> {result!r}")
        return result
    return clocked

Les décorateurs

Faire ses décorateurs paramétrés.

Notes:

Leur faire implémenter @memoize qui prend en paramètre une limite.

En profiter pour parler de global, nonlocal, et des closures.

Les décorateurs

Les utiliser pour leurs effets de bord.

Notes:

@route("/") par exemple.

Les décorateurs

  • @staticmethod
  • @classmethod
  • @property

contextlib

  • with suppress:
  • @contextmanager

The Walrus Operator

:=

Notes:

Démo REPL avec re.match, rappeler que les parenthèses sont souvent obligatoires.

Les listes en compréhension

l = []
for i in range(5):
  if i % 2 == 0:
    for j in range(5):
      if j % 2 == 0:
        for k in range(5):
          if k % 2 == 0:
            if i + j + k == 4:
              l.append((i,j,k))

Les listes en compréhension

>>> [(i, j, k)
...  for i in range(5)
...  if i % 2 == 0
...  for j in range(5)
...  if j % 2 == 0
...  for k in range(5)
...  if k % 2 == 0
...  if i + j + k == 4]
[(0, 0, 4), (0, 2, 2), (0, 4, 0), (2, 0, 2), (2, 2, 0), (4, 0, 0)]

Notes:

Juste pour doctest:

factors = lambda i: [i]

Les listes en compréhension

{x: factors(x)
 for x in range(1000)
 if len(factors(x)) == 3}

Notes: si factors est lent (spoiler: il l'est), c'est du gâchis, utiliser un walrus !

Les listes en compréhension

{x: prime_factors
 for x in range(1000)
 if len(prime_factors := factors(x)) == 3}

L'encodage

Les octets d'abord

>>> bytes([0x01, 0x02]) == b"\x01\x02"
True

Notes:

Notez qu'en hexadecimal, deux symboles permet de représenter exactement 8 bits, donc exactement un octet.

ASCII

Notes:

1960, 7 bits ("a word", qu'on a traduit "un octet"), [0; 127]

Seul la moitié des octets sont donc de l'ASCII valide.

Exercice: Utiliser range() et bytes([i]) pour afficher la table ascii.

Latin-1

Notes:

1985, 8 bits, [0; 255]

Couvre environ 32 langues.

Quasi complet pour le francais, il manque juste le Œ, le œ (le francais qui s'en est occupé n'était pas linguiste.)

Exercice: Utiliser range() et bytes([i]) pour afficher la table latin-1.

Unicode

Notes:

~1990, d'abord sur 16 bits, aujourd'hui c'est juste une base de donnée.

Couvre environ 150 langues (environ toutes).

Calque latin1 de 0 à 255, même C0 (controles bien définis) et C1 (controles ignorés, de 0x80 à 0x9F).

encoder, décoder

  • str.encodebytes
  • bytes.decodestr

Le packaging

Petite parenthèse

La différence entre un paquet et un module ?

Notes:

Pour Python il n'y en a pas, tout est module, pour nous, un paquet est un dossier. Aborder rapidement les paquets-espace-de-noms.

Digression

__main__ et __main__.py.

venv

Notes:

Et ses alternatives : virtualenv / conda.

pip

Notes:

Jamais sudo, toujours dans un venv.

pyproject.toml

pip install -e .

Packager

pip install build
python -m build

Publier

pip install twine
twine upload dist/*

Bonnes habitudes

There are 2 hard problems in computer science: cache invalidation, naming things, and off-by-1 errors.

Bonnes habitudes

Pas plus de 7.

Garder son API évolutive

Utilisez correctement / et * dans les prototypes de fonction.

Notes:

help(sum)

Les « linters »

Il existe plusieurs outils pour « relire » votre code :

  • flake8,
  • pylint,
  • mypy,
  • black,
  • bandit,
  • isort,
  • ruff,
  • tox.

Notes: Leur faire implémenter un is_prime(x) pour jouer avec.

pdb

breakpoint()

PYTHONDEVMODE=y

Et ./configure --with-pydebug.

async / await

Une coroutine est une fonction dont l'exécution peut être suspendue.

Callback Hell

function pong_handler(client)
{
    client.on('data', function (data)
    {
        client.on('data_written', function ()
        {
            client.close()
        });
        client.write(data)
        client.flush()
    });
}

Avec des coroutines

async def pong_handler():
    client.write(await client.read())
    await client.flush()
    client.close()

Les coroutines

  • generator-based coroutines
  • native coroutines

Generator-based coroutines

import types


@types.coroutine
def get_then_print(url):
    ...

Native coroutines

async def get_then_print(url):
    ...

Coroutines

Une coroutine, renvoie un objet coroutine :

>>> async def tum():
...     print("tum")
...
>>> tum()
<coroutine object tum at 0x7fa294538468>

Coroutines

>>> async def tum():
...     print("tum")
...
>>> a_coroutine_object = tum()
>>> a_coroutine_object.send(None)
tum
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  StopIteration

Notes:

qu'on peut manipuler.

As you can see, calling tum() did not execute the print("tum"), but calling .send(None) did (see PEP 342).

L'appel de .send est fait par la main loop (asyncio.run).

Récupérer un résultat

Le résultat d'une coroutine est stocké dans l'exception StopIteration.

Notes:

Dans l'attribut value.

await

async def two():
    return 2

async def four():
    return await two() + await two()

coro = four()
coro.send(None)

Notes:

Ça donne StopIteration: 4, de manière complètement synchrone.

Suspendre une coroutine.

Ce n'est pas possible dans une coroutine.

Notes:

Bon, à part await asyncio.sleep(0), ou toute attente vers un awaitable qui se suspend sans rien faire.

Future-like object

Un future-like object est un object implémentant __await__, qui a le droit de yield. L'expression du yield traversera toute la stack d'await jusqu'au send(None).

Awaitables

Les awaitables sont des objets pouvant être « attendus » via un await.

Notes:

Typiquement coroutine ou un objet implémentant __await__.

Gérer ses coroutines

async def two():
    return 2

async def four():
    return await two() + await two()

def coro_manager(coro):
    try:
        coro.send(None)
    except StopIteration as stop:
        return stop.value
>>> print(coro_manager(four()))
4

Gérer ses coroutines

class Awaitable:
    def __await__(self):
        yield

async def wont_terminate_here():
    await Awaitable()
    print("Terminated")
    return 42

coro_manager(wont_terminate_here())

Gérer ses coroutines

def frenetic_coro_manager(coro):
    try:
        while True:
            coro.send(None)
    except StopIteration as stop:
        return stop.value

Gérer ses coroutines

import random


def frenetic_coros_manager(*coros):
    coros = list(coros)
    while coros:
        coro = random.choice(coros)
        try:
            coro.send(None)
        except StopIteration as stop:
            coros.remove(coro)

Gérer ses coroutines

async def tum():
    while True:
        await Awaitable()
        print("Tum")

async def pak():
    while True:
        await Awaitable()
        print("Pak")

frenetic_coros_manager(tum(), pak())

Performance

Le code

def main():
    already_checked = []
    while True:
        c = "".join(choice(ascii_letters) for _ in range(10))
        if c in already_checked: continue
        already_checked.append(c)
        digest = sha512(
            (c + args.string).encode("UTF-8")).hexdigest()
        if digest.startswith(args.sha_prefix):
            print(f"sha512({c} + {args.string}) = {digest}")
            sys.exit(0)
        print("Searching")

Premiers tests

$ time python perf.py AFPy 00
Searching
[...]
Searching
Found: sha512(5NX3dB0BrO + AFPy) = 00…

real 0m0.048s
user 0m0.040s
sys 0m0.008s

Premiers tests

$ time python perf.py AFPy 000
Searching
[...]
Searching
Found: sha512(UYb0z6nac1 + AFPy) = 000…

real 0m2.797s
user 0m2.773s
sys 0m0.024s

Premiers tests

$ time python perf.py AFPy 0000
Searching
[...]
Searching
Found: sha512(dX0oAzvOmm + AFPy) = 0000…

real 0m16.381s
user 0m16.375s
sys 0m0.004s

C'est long mais ça passe …

Premiers tests

$ time python perf.py AFPy 00000
Searching
[...]
Searching
Searching
Searching
Searching

Bon, on a un sushi.

cProfile

$ python -m cProfile -o prof perf.py AFPy 0000

pstats

$ python -m pstats prof
Welcome to the profile statistics browser.
prof% sort cumulative
prof% stats 10

pstats

ncalls  cumtime  percall filename:lineno(function)
  12/1   17.007   17.007 {built-in method builtins.exec}
     1   17.007   17.007 /tmp/perf.py:1(<module>)
     1   16.996   16.996 /tmp/perf.py:20(main)
 36429    0.869    0.000 {method 'join' of 'str' objects}

snakeviz

$ pip install snakeviz
Collecting snakeviz
  Using cached snakeviz-2.1.0-py2.py3-none-any.whl (282 kB)
Collecting tornado>=2.0
  Using cached tornado-6.1-cp39-cp39-manylinux2010_x86_64.whl (427 kB)
Installing collected packages: tornado, snakeviz
Successfully installed snakeviz-2.1.0 tornado-6.1

snakeviz

$ snakeviz prof

snakeviz

vprof

$ pip install vprof
Collecting vprof
  Using cached vprof-0.38-py3-none-any.whl (319 kB)
Collecting psutil>=3
  Using cached psutil-5.7.3-cp39-cp39d-linux_x86_64.whl
Installing collected packages: psutil, vprof
Successfully installed psutil-5.7.3 vprof-0.38

vprof

$ vprof -c h "perf.py AFPy 0000"

vprof

Le code, v1

def main():
    already_checked = []
    while True:
        c = "".join(choice(ascii_letters) for _ in range(10))
        if c in already_checked: continue
        already_checked.append(c)
        digest = sha512(
            (c + args.string).encode("UTF-8")).hexdigest()
        if digest.startswith(args.sha_prefix):
            print(f"sha512({c} + {args.string}) = {digest}")
            sys.exit(0)
        print("Searching")

Le code, v2

def main():
    already_checked = set()
    while True:
        c = "".join(choice(ascii_letters) for _ in range(10))
        if c in already_checked: continue
        already_checked.add(c)
        digest = sha512(
            (c + args.string).encode("UTF-8")).hexdigest()
        if digest.startswith(args.sha_prefix):
            print(f"sha512({c} + {args.string}) = {digest}")
            sys.exit(0)
        print("Searching")

Les perfs

$ hyperfine 'python perf.py AFPy 00000'
  • v1 : ∞
  • v2 (set) : 23 s ± 23 s

::: notes

Il existe aussi pyperf: https://github.com/psf/pyperf

cProfile + pstats

$ python -m cProfile -o prof perf.py AFPy 0000
$ python -m pstats prof

cProfile + pstats

  ncalls  cumtime  percall filename:lineno(function)
    12/1    1.156    1.156 {built-in method builtins.exec}
       1    1.156    1.156 perf.py:1(<module>)
       1    1.143    1.143 perf.py:35(main)
   34215    0.771    0.000 {method 'join' of 'str' objects}
  371647    0.681    0.000 perf.py:39(<genexpr>)
  337860    0.526    0.000 /python3.9/random.py(choice)
  337860    0.283    0.000 /python3.9/random.py(randbelow)
   33786    0.134    0.000 built-in method print
  372745    0.037    0.000 method 'getrandbits' of Random'
   33786    0.037    0.000 method 'hexdigest' of hashlib

snakeviz

$ snakeviz prof

snakeviz

Le code, v2

def main():
    already_checked = set()
    while True:
        c = "".join(choice(ascii_letters) for _ in range(10))
        if c in already_checked: continue
        already_checked.add(c)
        digest = sha512(
            (c + args.string).encode("UTF-8")).hexdigest()
        if digest.startswith(args.sha_prefix):
            print(f"sha512({c} + {args.string}) = {digest}")
            sys.exit(0)
        print("Searching")

Le code, v3

def main():
    already_checked = set()
    while True:
        c = "".join(choices(ascii_letters, k=10))
        if c in already_checked: continue
        already_checked.add(c)
        digest = sha512(
            (c + args.string).encode("UTF-8")).hexdigest()
        if digest.startswith(args.sha_prefix):
            print(f"sha512({c} + {args.string}) = {digest}")
            sys.exit(0)
        print("Searching")

Les perfs

$ hyperfine 'python perf.py AFPy 00000'
  • v1 : ∞
  • v2 (set) : 23 s ± 23 s
  • v3 (choices): 8.591 s ± 6.525 s

snakeviz

Le code, v4

def main():
    already_checked = set()
    for c in product(ascii_letters, repeat=10):
        c = "".join(c)
        if c in already_checked: continue
        already_checked.add(c)
        digest = sha512(
            (c + args.string).encode("UTF-8")).hexdigest()
        if digest.startswith(args.sha_prefix):
            print(f"sha512({c} + {args.string}) = {digest}")
            sys.exit(0)
        print("Searching")

Les perfs

$ hyperfine 'python perf.py AFPy 00000'
  • v1 : ∞
  • v2 (set) : 23 s ± 23 s
  • v3 (choices): 8.591 s ± 6.525 s
  • v4 (deterministic) : 3.900 s ± 0.121 s

snakeviz

Le code, v5

def main():
    already_checked = set()
    for c in product(ascii_letters, repeat=10):
        c = "".join(c)
        if c in already_checked: continue
        already_checked.add(c)
        digest = sha512(
            (c + args.string).encode("UTF-8")).hexdigest()
        if digest.startswith(args.sha_prefix):
            print(f"sha512({c} + {args.string}) = {digest}")
            sys.exit(0)
        # print("Searching")

Les perfs

$ hyperfine 'python perf.py AFPy 00000'
  • v1 : ∞
  • v2 (set) : 23 s ± 23 s
  • v3 (choices): 8.591 s ± 6.525 s
  • v4 (deterministic) : 3.900 s ± 0.121 s
  • v5 (print) : 3.120 s ± 0.062 s

Snakeviz

Il reste du hexdigest, du encode, et du join.

vprof

Ligne 26 et 28 !?

Le code, v6

def main():
    for c in product(ascii_letters, repeat=10):
        c = "".join(c)
        digest = sha512(
            (c + args.string).encode("UTF-8")).hexdigest()
        if digest.startswith(args.sha_prefix):
            print(f"sha512({c} + {args.string}) = {digest}")
            sys.exit(0)

Snakeviz

Il reste du hexdigest, du encode, et du join.

Le code, v7

def main():
    string = args.string.encode("UTF-8")
    pool = ascii_letters.encode("UTF-8")
    for c in product(pool, repeat=10):
        digest = sha512(bytes(c) + string).hexdigest()
        if digest.startswith(args.sha_prefix):
            print(f"sha512({bytes(c)} + {args.string}) = "
                  f"{digest}")
            sys.exit(0)

Les perfs

$ hyperfine 'python perf.py AFPy 00000'
  • v1 : ∞
  • v2 (set) : 23 s ± 23 s
  • v3 (choices): 8.591 s ± 6.525 s
  • v4 (deterministic) : 3.900 s ± 0.121 s
  • v5 (print) : 3.120 s ± 0.062 s
  • v6 (dead code): 2.844 s ± 0.059 s
  • v7 (bytes) : 1.837 s ± 0.067 s

Encore plus d'expériences

  • pypy: 3.8s
  • python: 1.8s
  • cython (hashlib) 1.3s
  • cython (crypto) 0.8s
  • c: 0.3s