## Python avancé ### Présenté par Julien Palard
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 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
>>> 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 "
", line 1, in
__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
## 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()
```
## Coroutines ``` >>> async def tum(): ... print("tum") ... >>> a_coroutine_object = tum() >>> a_coroutine_object.send(None) tum Traceback (most recent call last): File "
", line 1, in
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(
) 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 
## 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 ```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
## 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(
) 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(
) 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 
## 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 
## 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 
## 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  Il reste du `hexdigest`, du `encode`, et du `join`.
## vprof  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  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