Ce billet était initialement paru sur le blog Pydéfis.

Mot clé static en C

Ce qu’on veut faire ici en Python existe en C :

void compteur(void){
    static int c = 0;
    c = c + 1;
    printf("Appel numéro : %d\n",c);
}

void main(void) {
    int i;
    for(i=0;i<3;i++){
    compteur();
    }
}

L’exécution du programme précédent provoque cet affichage :

Appel numéro : 1
Appel numéro : 2
Appel numéro : 3

La variable statique c est locale à la fonction compteur mais elle conserve sa valeur entre 2 appels (grâce au mot clé static).

En Python, on ne dispose pas de ce mécanisme mais…

Monkey patching Python

Python permet de faire du «monkey patching», c’est à dire de rajouter (ou modifier) des attributs à n’importe quel objet, à la volée. De plus, une fonction est un objet. On va donc rajouter notre variable comme attribut à la fonction :

def compteur():
    compteur.c = compteur.c + 1
    print("Appel numéro : ", c)

compteur.c = 0

for i in range(3):
    compteur()

Ce code a le même effet que le programme C précédent… mais la ligne compteur.c = 0 n’est pas terrible. On doit la mettre, sinon, l’expression compteur.c + 1 au début de la fonction ne peut pas être évaluée. On préférerait la mettre avant la fonction compteur, mais ce n’est pas possible puisque pour écrire compteur.c il faut que la référence compteur (à la fonction) soit définie. Bien sûr la ligne ne peut pas non plus figurer telle qu’elle dans la fonction, sinon le compteur c serait réinitialisé à 0 à chaque appel. On peut tout de même arranger un peu le code comme ceci:

def compteur():
    if not hasattr(compteur, 'c'):
        compteur.c = 0
    compteur.c = compteur.c + 1
    print("Appel numéro : ", c)

for i in range(3):
    compteur()

Cette fois, l’attribut c est ajouté et initialisé dans la fonction (compteur.c = 0), s’il n’existe pas déjà. Lors du second appel à la fonction compteur, puisque l’attribut c existe déjà, on ne fait rien de spécial…

Ce billet pourrait se terminer ici, mais c'est aussi l'occasion de parler des décorateurs. Le problème n'est pas pris par le début ici, et si vous n'êtes pas familier des décorateurs, la suite risque de vous paraître un peu obscure. Si c'est le cas, vous trouverez des informations ici: Les décorateurs (OpenClassroom)

Avec un décorateur ?

On peut aussi proposer une solution avec décorateurs:

def static(dict_var_val):
    def staticf(f):    
        def decorated(*args, **kwargs):
            #print("Appel decorated", args, kwargs)
            f(*args, **kwargs)
            for var, val in dict_var_val.items():
                setattr(decorated, var, val)
        return decorated
    return staticf

Le décorateur, ici nommé static est un peu technique (précisément static est une fonction qui renvoie un décorateur…). Voilà quelques éléments pour aider à sa compréhension : la fonction static prend en paramètre un dictionnaire (les variables statiques qu’on veut utiliser avec leur valeur initiale). Cette fonction (static) renvoie un décorateur (staticf) et ce décorateur prend en paramètre une fonction (la fonction à décorer f) et renvoie la fonction decorated qui contient juste un appel à la fonction f avec ses paramètres, et qui est «monkey patchée» :-) par la boucle contenant setattr(...).

Le décorateur est très simple à utiliser, comme c’est souvent le cas avec les décorateurs (l’effort est fait au moment de la conception) :

@static({'c': 0})
def compteur():
    compteur.c += 1
    print(compteur.c)

for i in range(3):
    compteur()

On peut avoir plusieurs variables statiques:

@static({'c': 0, 'p': 1})
def compteur2():
    compteur2.c += 1
    compteur2.p *= 2
    print("c, p:", compteur2.c, compteur2.p)

for i in range(3):
    compteur2()

L’exécution donne :

c, p: 1 2
c, p: 2 4
c, p: 3 8

Notons au passage que, si l’on écrit des décorateurs, il est avantageux de regarder du côté de functools.wraps, lui-même un décorateur, qui permet de préserver les docstrings (et d’autres informations) dans les fonctions décorées.