24 KiB
Python avancé
Présenté par
Julien Palard julien@palard.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é
- Pas de
- 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 !
- Pas de
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:
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.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
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