# Python avancé

### Présenté par

<!-- .slide: data-background="static/background.jpg" -->

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 ?

```python
>>> (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

```python
>>> 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 :

```python
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

```pycon
>>> 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 :

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


## Ne pas confondre

```python
>>> 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 ?

```python
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`

```python
>>> 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 »

```python
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

```python
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

```python
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

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

Notes:

Expliquer le protocole.


## Les gestionnaires de contexte

```python
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:
```python
def clock(f=None, *args, **kwargs):
    return lambda *args: None
```

## Les décorateurs

```python
@clock
def fib(n):
    ...
```

équivaut à

```python
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

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

équivaut à

```python
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.
```python
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

```python
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

```python
>>> [(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:

```python
factors = lambda i: [i]
```

## Les listes en compréhension

```python
{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

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


# L'encodage

## Les octets d'abord

```python
>>> 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.encode` → `bytes`
- `bytes.decode` → `str`


# 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

- https://setuptools.readthedocs.io/
- https://github.com/JulienPalard/oeis


## pip install -e .

## Packager

```bash
pip install build
python -m build
```

### Publier

```bash
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

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

## Les coroutines

 - generator-based coroutines
 - native coroutines


## Generator-based coroutines

```pytho
import types


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


## Native coroutines

```python
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


```text
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](https://www.python.org/dev/peps/pep-0492/#await-expression)
sont des objets pouvant être « attendus » via un `await`.

Notes:

Typiquement `coroutine` ou un objet implémentant `__await__`.


## Gérer ses coroutines

```python
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
```

```pycon
>>> print(coro_manager(four()))
4
```


## Gérer ses coroutines


```python
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


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


## Gérer ses coroutines

```python
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

```text
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

```python
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

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

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

## Premiers tests

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

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

## Premiers tests

```bash
$ 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

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

Bon, on a un sushi.

## cProfile

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

## pstats

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

## pstats

```txt
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

```bash
$ 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

```bash
$ snakeviz prof
```

## snakeviz
![](static/snakeviz-v1.png)


## 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

![](static/vprof.png)


## Le code, v1

```python [2,5,6]
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

```python [2,5,6]
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

```bash
$ 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

```bash
$ 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

```bash
$ snakeviz prof
```

## snakeviz

![](static/snakeviz-v2.png)


## Le code, v2

```python [4]
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

```python [4]
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

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


## snakeviz

![](static/snakeviz-v3.png)


## Le code, v4

```python [3]
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

```bash
$ 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

![](static/snakeviz-v4.png)


## Le code, v5

```python [12]
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

```bash
$ 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

![](static/snakeviz-v5.png)

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

## vprof

![](static/vprof2.png)

Ligne 26 et 28 !?

## Le code, v6

```python
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

![](static/snakeviz-v6.png)

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


## Le code, v7

```python
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

```bash
$ 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
