Pièges communs

Pour la plus grande partie, Python vise à être un langage propre et cohérent qui permet d’éviter des surprises. Cependant, il y a quelques cas qui peuvent être sources de confusion pour les nouveaux arrivants.

Certains de ces cas sont intentionnels, mais peuvent être potentiellement surprenants. Certains pourraient sans doute être considérés comme des verrues du langage. En général, ce qui suit est une collection de comportements potentiellement délicats qui pourraient sembler étranges à première vue, mais qui sont généralement raisonnables une fois que vous êtes au courant de la cause sous-jacente de cette surprise.

Arguments par défaut mutables

Apparemment la surprise la plus commune que les nouveaux programmeurs Python rencontre est le traitement Python des arguments par défaut mutables dans les définitions de fonction.

Ce que vous écrivez

def append_to(element, to=[]):
    to.append(element)
    return to

Qu’est-ce que vous auriez pu attendre qu’il se passe

my_list = append_to(12)
print my_list

my_other_list = append_to(42)
print my_other_list

Une nouvelle liste est créée chaque fois que la fonction est appelée si un second argument n’est pas fourni, de sorte que la sortie est:

[12]
[42]

Ce qui se passe

[12]
[12, 42]

Une nouvelle liste est créée une seule fois quand la fonction est définie, et la même liste est utilisée dans chaque appel successif.

Les arguments par défaut de Python sont évalués une seule fois lorsque la fonction est définie, pas chaque fois que la fonction est appelée (comme c’est le cas, disons en Ruby). Cela signifie que si vous utilisez un argument par défaut mutable et le mutez, vous aurez muté l’argument ici et pour tous les futurs appels à la fonction aussi.

Ce que vous devriez faire à la place

Créez un nouvel objet à chaque fois que la fonction est appelée, en utilisant un argument par défaut pour signaler que aucun argument n’a été fourni (None est souvent un bon choix).

def append_to(element, to=None):
    if to is None:
        to = []
    to.append(element)
    return to

Quand le piège n’est pas un piège

Parfois, vous pouvez spécifiquement “exploiter” (lisez: utilisé comme prévu) ce comportement pour maintenir l’état entre les appels d’une fonction. C’est souvent fait lors de l’écriture d’une fonction de mise en cache.

Closures des bindings tardives

Une autre source de confusion est la manière dont Python bind ses variables dans les closures (ou dans la portée globale entourante)

Ce que vous écrivez

def create_multipliers():
    return [lambda x : i * x for i in range(5)]

Qu’est-ce que vous auriez pu attendre qu’il se passe

for multiplier in create_multipliers():
    print multiplier(2)

Une liste contenant cinq fonctions qui ont chacun leur propre variable i fermée sur elle-même qui multiplie leur argument, produisant:

0
2
4
6
8

Ce qui se passe

8
8
8
8
8

Cinq fonctions sont créées; au lieu que toutes ne multiplient juste x par 4.

Les closures de Python sont des late binding. Cela signifie que les valeurs des variables utilisées dans les closures sont regardées au moment où la fonction interne est appelée.

Ici, chaque fois que n’importe lesquelles des fonctions retournées sont appelées, la valeur de i est recherché dans la portée environnante au moment de l’appel. D’ici là, la boucle est terminée et i est laissé à sa valeur finale de 4.

Ce qui est particulièrement déplaisant sur ce piège est la désinformation apparemment répandue que cela a quelque chose à voir avec les lambdas en Python. Les fonctions créées avec une expression lambda ne sont en aucune façon particulière, et en fait le même comportement est exposé en utilisant simplement un ordinaire def:

def create_multipliers():
    multipliers = []

    for i in range(5):
        def multiplier(x):
            return i * x
        multipliers.append(multiplier)

    return multipliers

Ce que vous devriez faire à la place

La solution la plus générale est sans doute une forme de hack. En raison du comportement de Python déjà mentionné concernant l’évaluation des arguments par défaut aux fonctions (voir Arguments par défaut mutables), vous pouvez créer une closure qui se bind immédiatement à ses arguments en utilisant un argument par défaut comme ceci:

def create_multipliers():
    return [lambda x, i=i : i * x for i in range(5)]

Alternativement, vous pouvez utiliser la fonction functools.partial:

from functools import partial
from operator import mul

def create_multipliers():
    return [partial(mul, i) for i in range(5)]

Quand le piège n’est pas un piège

Parfois, vous voulez que vos closures se comportent de cette façon. Les late binding sont biens dans beaucoup de situations. Boucler pour créer des fonctions uniques est malheureusement un cas où ils peuvent causer le hoquet.

Fichiers Bytecode (.pyc) partout!

Par défaut, lors de l’exécution du code Python à partir de fichiers, l’interpréteur Python va automatiquement écrire une version bytecode de ce fichier sur le disque, par exemple module.pyc.

Ces fichiers .pyc ne doivent pas être versionnés dans vos dépôts de code source.

Théoriquement, ce comportement est activé par défaut, pour des raisons de performance. Sans ces fichiers bytecode présents, Python regénérerait le bytecode chaque fois que le fichier est chargé.

Désactiver les fichiers Bytecode (.pyc)

Heureusement, le processus de génération du bytecode est extrêmement rapide, et n’est pas quelque chose dont vous avez besoin de vous soucier quand vous développez votre code.

Ces fichiers sont ennuyeux, débarrassons-nous d’eux!

$ export PYTHONDONTWRITEBYTECODE=1

Avec la variable d’environnement $PYTHONDONTWRITEBYTECODE définie, Python n’écrira pas plus longtemps ces fichiers sur le disque, et votre environnement de développement restera agréable et propre.

Je recommande la définition de cette variable d’environnement dans votre ~/.profile.

Enlever les fichiers Bytecode (.pyc)

Voici une astuce sympathique pour enlever tous ces fichiers, s’ils existent déjà:

$ find . -name "*.pyc" -delete

Exécutez ceci depuis le répertoire racine de votre projet, et tous les fichiers .pyc vont soudainement disparaître. Beaucoup mieux.