Python 5

Décorateurs

Un décorateur sert à envelopper un fonction dans une autre:

@dec
def f(arg1, arg2):
    pass

est équivalent à

def f(arg1, arg2):
    pass
f = dec(f)

dec prend une fonction comme argument et retourne une fonction.

Un petit test pour comprendre ce qui se passe :

Exemple : mise en cache des valeurs d'une fonction récursive (memoization)

Permet d'automatiser le procédé, déjà illustré sur l'exemple de la suite de Fibonacci.

Sans décorateur :

Avec :

En Python 3, le module functools exporte un décorateur, @lru_cache, qui construit un cache LRU.

Exemple : mesure du temps d'éxécution d'une fonction :

Exemple : vérifier le type d'un argument :

Les décorateurs peuvent être empilés :

On remarque que l'erreur est attribuée à la fonction wrapper. Si on n'a pas sous les yeux le code des décorateurs utilisés, on peut chercher longtemps l'origine du problème ...

Décorateurs avec arguments

pytyhon
@dec(argA, argB)
def f(arg1, arg2):
    pass

est équivalent à

def f(arg1, arg2):
    pass
f = dec(argA, argB)(f)

C'est donc équivalent à créer une fonction composée f = dec(argA, argB)(f)

Autrement dit, dec(argA, argB) doit être un décorateur.

Exemple : ajouter un attribut à une fonction :

Exemple : tester le type de la valeur retournée par une fonction :

Décorateurs définis par des classes

La seule contrainte sur l'objet retourné par un décorateur est qu'il se comporte comme une fonction (duck typing), autrement dit qu'il soit callable.

C'est le cas de toute classe possédant la méthode spéciale __call__.

class MyDecorator(object):
    def __init__(self, f):
    # faire quelquechose avec f ...
   def __call__(*args):
    # faire autre chose avec args

Exemple : permettre à une fonction de compter combien de fois elle a été appélée :

Méthodes de classes et méthodes statiques

On a déjà vu la différence entre les variables des classes et celles des instances :

Les méthodes normales sont des méthodes d'instances. Leur premier argument doit être l'instance elle-même, conventionnellement appelée self.

Il existe aussi des méthodes de classes. On les définit comme les méthodes d'instance, leur premier argument est alors la classe elle-même, conventionnellement appelée cls, puis on les passe à la fonction classmethod :

C'est plus clair avec un décorateur :

Par exemple, la classe B qui tient un compte de ses instances pourrait s'écrire

Une méthode statique ne prend ni une instance ni la classe comme premier paramètre. Elle se définit à l'aide de la fonction staticmethod ou du décorateur @staticmethod.

Itération, itérateurs et itertools

Rappel :

Normalement, on écrit plûtôt

On peut définir des classes qui supportent l'itération : il suffit d'implémenter les méthodes __iter__ et __next__.

Les boucles longues sont peu efficaces et sont une des principales causes de lenteur en Python.

Pour des boucles sur les entiers, en Python 2, on utilisera xrange (un générateur écrit directement en C) plutôt que range.

Pour des itérations plus compliquées, on pourra utiliser le module itertools, qui propose des version optimisées d'opérations courantes, et de nombreuses fonctionnalités commodes.

On peut chainer des itérateurs :

ou les tricoter ...

Compteurs

count(start,step=1) engendre les entiers à partir de start avec les pas step.

La fonction islice(iterable,[start],stop,[step]) remplace iterable[start:stop:step]:

Prooduit cartésien

Avec les mots binaires de 5 digits, on pourrait faire

La fonction product renvoie le produit cartésien (les tuples) d'un nombre arbitraire d'itérables.

On peut s'en servir pour construire les mots de longueur donnée sur un alphabet, comme dans ce craqueur de mots de passe basique :

Ce n'est pas très efficace (!) mais c'est simple ...

tee

La fonction tee(iterateur,n=2) retourne $n$ copies identiques de l'itérateur :

starmap

La fonction starmap fonctionne comme map, mais calcule f(*i) :

cycle

La fonction cycle répète indéfiniment un itérateur fini :

repeat

La fonction repeat fait ce qu'on imagine :

On l'utilise en combinaison avec map ou zip :

Filtrage

Le filtrage s'effectue au moyen des fonctions dropwhile, takewhile, filter, filterfalse :

groupby

Plus complexe : groupby construit un itérateur qui renvoie les clés et groupes consécutifs d'un itérable.La clé key est une fonction qui calcule une valeur sur chaque élément. Par défaut, c'est l'identité.

On pourra donc écrire

Le résultat complet serait

Finalement, on dispose de quelques fonctions combinatoires basiques :

Autres optimisations

Le module operator propose des fonctions optimisées pour remplacer les opérateurs standards de Python (ex. add(x,y)).

Le module collections fournit des structures de données hautes performances pour remplacer dict, list, set, tuple : namedtuple(), deque, Counter, OrderedDict, defaultdict.

Le module array fournit des tableaux optimisés pour des types de données basiques (caractères, entiers, flottants ...)

Le module ctypes

Il permet d'utiliser des bibliothèques partagées, avec des types de données compatibles au C.

...
[I 08:28:51.990 NotebookApp] Saving file at /python3_M1_2020-5.ipynb
Hello world!
[I 08:38:52.001 NotebookApp] Saving file at /python3_M1_2020-5.ipynb
...

Le résultat s'est affiché sur le terminal dans lequel on a lancé l'interface graphique ...

Dans un terminal, on obtiendrait

>>> from ctypes import *
>>> libc =  cdll.LoadLibrary("libc.so.6")
>>> printf=libc.printf
>>> printf(b"%s\n", b"Hello world!")
Hello world!
13
>>>

Pour de longues itérations, on peut écrire en C la fonction critique et la compiler sous forme dll/shared object

Un petit test de performances :

/* rien.c
 compiler avec
   gcc -Wall -fPIC -c rien.c
   gcc -shared -Wl,-soname,librien.so.1 -o librien.so.1.0   *.o
*/

int rien(int n){
    int i=0;
    while (1==1) {
        i++;
        if(i>n){ return(i); }
    }
}

Cython

Pour étendre Python avec du code C ou C++, il vaut mieux utiliser Cython

Après avoir installé Cython, on peut reprendre l'exemple précédent. On crée un fichier rien.pyx

# rien.pyx`
def rien(int n):
    cdef int i=0
    while 1==1:
        i+=1
        if i>n: return i

Le code est le même, à ceci près que les types int de n et i ont été déclarés.

Pour compiler, il faut, dans le même répertoire, un fichier setup.py structuré ainsi :

# setup.py
from distutils.core import setup
from Cython.Build import cythonize

setup( ext_modules = cythonize("rien.pyx") )

On compile avec la commande

python setup.py build_ext --inplace

On peut alors importer rien comme un module ordinaire

468 fois plus rapide ici, donc. Notons au passage que si on n'avait pas déclaré les types, l'effet aurait été beaucoup moins bon (un facteur 2).