Valeurs et références en Python


Petit article en complément de l’article de Réchèr.

Il y a plusieurs manières de passer une variable en informatique: par valeur ou par référence. Et dans les langages bas niveau comme le C, on se pose la question: “passe-t-on la valeur ? un pointer ? un pointer vers un pointer ?”

En Python ce n’est pas la question puisque tout se passe par référence. Tous les objets. Dans tous les cas.

La question est donc plutôt: “ça veut dire quoi passer par référence ?”

Assignation et référence

Une référence est comme une adresse qui dit où la donnée se trouve en mémoire.

Quand vous faites ceci:

a = [1, 2, 3]

Vous n’assignez pas la liste à la variable “a”, vous assignez une référence vers la liste, donc une sorte d’adresse qui indique où elle se trouve en mémoire. (en vérité vous n’assignez rien, les variables sont des étiquettes en Python, mais on va ignorer ce détail ici).

Et quand vous faites:

print a

Python va chercher dans a la référence, et retrouver la liste en suivant “l’adresse”.

C’est important car ça veut dire deux choses.

1. Quand vous faites ça:

b = a

Vous ne copiez pas la liste. Vous copiez la référence. Du coup, on ne prend pas deux fois la place en mémoire, et la copie est très rapide

2. Quand vous faites ça:

b = a

Vous ne copiez pas la liste. Vous copiez la référence. Bis.

Et si vous faites:

b.append(4)

Alors:

print a

Va donner…

[1, 2, 3, 4]

Car en faisant append() sur b, Python va trouver une référence, retrouver la liste derrière la référence, et faire un append() sur la liste. Comme c’est la même réference dans a et b (puisqu’on l’a copié), c’est la même liste derrière. Tout ce qu’on applique à a, s’applique donc à b, et vice-versa.

Si vous voulez faire une vraie copie, alors il faut recréer une toute nouvelle liste. Par exemple:

b = list(a)

Mutable et non mutable

L’assignation par référence n’a vraiment d’importance que dans le cas où un objet est mutable. En Python, il existe en effet deux types d’objets: les mutables (listes, dictionnaires, sets, objets custo, etc) et les non mutables (strings, int, floats, tuples, etc).

Les mutables sont ceux qu’on peut modifier après leur création. Les non mutables sont ceux qu’on ne peut pas modifier après création.

On ne peut pas modifier 152 une fois que l’objet int est créé. Mais on peut rajouter des éléments à une liste après qu’elle soit créée. Les ints sont non mutables. Les lists sont mutables.

Cela peut surprendre, mais les strings sont non mutables. Même quand vous faites:

>>> pa = "papa"
>>> pi = pa.replace("a", "i")
>>> print pa
papa
>>> print pi
pipi

Vous ne modifiez pas la chaîne originale: vous créez une copie de la chaîne, et la chaîne de départ n’a pas bougé.

C’est important car si on fait une référence vers un type non mutable, on s’en oint le pourtour anal avec pelle à tarte: il ne peut pas être modifié. Peu importe que la référence soit copiée à droite et à gauche. Mais si l’objet est mutable, chaque copie de la référence ajoute un endroit dans le programme duquel on peut modifier l’objet.

Cela a des implications parfois assez coquines.

Ainsi:

>>> l = [0] * 3
>>> l # une liste de 3 zéros
[0, 0, 0]
>>> l[1] += 1
>>> l
[0, 1, 0]

Ici tout se comporte comme prévu. Les ints sont non mutables, donc on ne s’aperçoit pas d’un détail important: [0] * 3 copie la référence à 0 trois fois. Quand on fait +=, ça remplace l’ancien int par un nouveau, donc un seul item de la liste est changé.

Mais si on fait vicieusement:

>>> l = [[0]] * 3
>>> l # une liste de listes d'un seul zéro chacune
[[0], [0], [0]]
>>> l[1][0] += 1
>>> l
[[1], [1], [1]]

Ici on a copié 3 fois la référence vers la même liste. Du coup une modification affecte les 3 items. Doh.

Rappels: les tuples ne sont pas mutables. Et on peut passer d’un tuple à une liste avec tuple(l) et list(t). Pensez-y si vous rencontrez ce genre de problème.

Passage des arguments de fonction par référence

Quand vous faites cela:

def encore_une_fonction_d_exemple_inutile(l):
    l.append(4)
    return l
 
>>> l1 = [1, 2, 3]
>>> encore_une_fonction_d_exemple_inutile(l1)
>>> print l1
[1, 2, 3, 4]

Vous noterez que la liste a été modifiée. C’est parce que l’on passe une référence à la liste quand on la passe en argument. Toute modification de la liste dans la fonction est donc visible en dehors de la fonction. Si le paramètre était immutable, encore une fois on s’en ficherait. Mais là, comme liste est mutable, notre fonction possède ce qu’on nomme un effet de bord: quand on l’appelle, elle a des conséquences sur des objets qui existent en dehors d’elle même.

Il est généralement de bon ton d’éviter les effets de bord, aussi, essayez toujours de travailler sur des copies: utilisez le slicing, les listes en intention, les générateurs, etc., pour retourner les nouvelles valeurs plutôt que de modifier l’objet original.

Ici:

def encore_une_fonction_d_exemple_inutile(l):
    return l1 + [4]

Nous permet de retourner une nouvelle liste. Si la liste prend beaucoup de place en mémoire, on peut faire ça:

def encore_une_fonction_d_exemple_inutile(l):
 
    for x in l:
        yield x
    yield 4

Ce qui aurait pour effet de retourner un itérable similaire, sans prendre plus de mémoire.

Valeurs par défaut et référence

Quand on utilise une valeur par défaut, par exemple dans la déclaration des paramètres d’une fonction, on initialise une référence qui va rester la même pour toute la durée du programme.

def encore_une_fonction_d_exemple_inutile(l=[1, 2, 3]):
    l.append(4)
    return l
 
>>> encore_une_fonction_d_exemple_inutile()
>>> encore_une_fonction_d_exemple_inutile()
>>> print l1
[1, 2, 3, 4, 4]

On constate ici que 4 a été ajouté deux fois dans la liste. En effet, l est l’argument par défaut, et ici, il est initialisé à la référence pointant sur une liste [1, 2, 3]. Pas sur la valeur [1, 2, 3]. [1, 2, 3] est stocké quelque part dans un monde invisible que seul Guido Van Rossum connait, et l contient juste la référence à cette liste.

Cette référence est gardée, et à chaque appel, c’est la même liste qui est utilisée. Si on appelle deux fois la fonction, la première fois c’est la liste [1, 2, 3, 4] à laquelle on ajoute 4. Puis comme c’est la même référence, on ajoute ensuite 4 à la même liste, à laquelle on avait déjà ajouté 4.

Bref, évitez les mutables dans les paramètres par défaut à moins de savoir ce que vous faites (cache ou memoization).

Variables de classe et références

La même problématique existe pour les classes. Si vous faites:

class AuChocolat(objet):
 
    supplements = ['chantilly', 'praline']
 
>>> g1 = AuChocolat()
>>> g2 = AuChocolat()
>>> g1.supplements.pop()
'praline'
>>> g2.supplements
['chantilly']

Même principe: toute variable de classe pointe pour toute la durée du programme sur la même réference. La référence va être partagée entre toutes les instances de la classe. C’est donc la même liste !

Maintient d’une référence

Python garde un objet en mémoire tant qu’il existe une référence vers cet objet.

>>> a = 1
>>> b = a
>>> del a
>>> print b
1

Si je supprime a, il reste b, donc l’objet est toujours en mémoire. Si je supprime b également, Python va lancer le processus de nettoyage pour effacer l’objet de la mémoire.

Une référence existe toujours dans le cadre d’un scope. Par exemple, si une référence est copiée dans une fonction, quand la fonction se termine, la copie de la référence est supprimée, et ne compte plus comme une voix pour le maintient de l’objet en mémoire.

En Python, il n’existe donc aucun moyen de supprimer un objet. On peut juste supprimer toutes ses références, et attendre que Python s’aperçoive que tout le monde s’en branle à présent de l’objet, et le supprime.

Dans certains cas particuliers, on veut qu’une référence à un objet ne compte pas comme une voix. On peut le faire en utilisant le module weakref, mais cela ne marche qu’avec les classes que l’on code soit-même.

class ALaVanille(object):
    pass
 
>>> import weakref
>>> g1 = ALaVanille()
>>> g2 = weakref.proxy(g1)
>>> g2
<weakproxy at 0x7f5f26994730 to ALaVanille at 0x7f5f26992d90>
>>> g1.foo = "bar"
>>> g2.foo
'bar'
>>> del g1
>>> g2
<weakproxy at 0x7f5f26994730 to NoneType at 0x859380>

Dès qu’on supprime g1, g2 proxy vers None, car la référence vers l’instance de ALaVanille ne compte pas pour garder en vie l’objet.

23 thoughts on “Valeurs et références en Python

  • ouhouhsami

    Je me demande si l’exemple donné :

    &gt;&gt;&gt; pa = "papa"
    &gt;&gt;&gt; pi = "papa".replace("a", "i")
    &gt;&gt;&gt; print pa
    papa
    &gt;&gt;&gt; print pi
    pipi

    ne serait pas plutôt

    &gt;&gt;&gt; pa = "papa"
    &gt;&gt;&gt; pi = pa.replace("a", "i")
    &gt;&gt;&gt; print pa
    papa
    &gt;&gt;&gt; print pi
    pipi

    ?

  • François

    Hello,

    Est ce qu’on peut espérer un prochain article sur le module copy et le moyen de contourner les trois problèmes soulevées sur la copie des listes ?

    Merci :)

  • Sam Post author

    Tout est possible. Tout est imaginable. C’est le jeu de la vie.

  • Etienne

    Intéressant tout ça. L’histoire des valeurs par défaut m’a bluffé, donc j’ai examiné ça de plus près. Conclusion: dans le code de ce paragraphe je suppose qu’il faudrait écrire quelque chose du genre
    l1 = encore_une_fonction_d_exemple_inutile()
    au moins une fois, histoire de passer la référence, l restant dans le scope de la fonction.

  • LB

    là, cet article je l’aime bien, j’ai l’impression d’être un expert. Un craneur aussi. La journée a été dure, faut bien avoir de petites satisfactions (certes un peu mesquines).

  • JeromeJ

    Je proxy, tu proxy, il proxy :) je savais pas que ça se disait, et c’est un bon exemple pour la dernière fois où on ne comprenait pas le terme.

  • Réchèr

    Quelques remarques, plus ou moins en rapport avec le commentaire d’etienne.

    Dans le premier exemple de la partie “Passage des arguments de fonction par référence”, il n’y a pas besoin de récupérer la valeur renvoyée. D’ailleurs, la fonction n’a même pas besoin de faire un return.

    Je viens de tester dans ma console, ça me fait un truc comme ça :

    def encore_une_fonction_d_exemple_inutile(l):
        l.append(4)
        # il n'y a pas de return ici !
     
    »» ll = [1, 2, 3]
    »» encore_une_fonction_d_exemple_inutile(ll)
    »» ll
    [1, 2, 3, 4]

    Pour le premier exemple de code de la partie “Valeurs par défaut et référence”, j’aurais plus vu un truc comme ça :

    def encore_une_fonction_d_exemple_inutile(l=[1, 2, 3]):
        l.append(4)
        return l
     
    # appel de la fonction, avec le paramètre par défaut.
    &gt;&gt;&gt; encore_une_fonction_d_exemple_inutile()
    # le résultat renvoyé est une liste avec le "4" ajouté à la fin. 
    [1, 2, 3, 4]
    # Jusqu'ici tout va bien. Sauf que ce premier appel a modifié
    # le contenu de la zone mémoire sur laquelle pointe 
    # le paramètre par défaut ! 
     
    # On rappelle la fonction, toujours avec le paramètre par défaut.
    &gt;&gt;&gt; encore_une_fonction_d_exemple_inutile()
    # Pouf ! la valeur renvoyée a changée (puisque le param 
    # par défaut a changé)
    [1, 2, 3, 4, 4]
    # etc.
    &gt;&gt;&gt; encore_une_fonction_d_exemple_inutile()
    [1, 2, 3, 4, 4, 4]

    D’autre part, je pense qu’il faut éviter d’utiliser la lettre “l” comme nom de variable. Parce qu’on la confond avec le chiffre 1. Oui c’est idiot, mais c’est comme ça. Quelqu’un connaît l’inventeur de la police Courier New que je lui mette mon pied dans la tronche ?

    Oh et puis, allons-y carrément : quelqu’un connaît l’inventeur de la lettre “L” ?

  • Etienne

    C’est l’homme qui est l’inventeur du L (C’est pas moi, c’est L).

  • Kontre

    Cet article me rappelle une question que je me pose régulièrement : c’est quoi la différence entre un pointeur et une référence ? Dans les deux cas, c’est un bidule qui indique où l’objet se trouve dans la mémoire.

    Y’a une coquille dans l’example suivant :

    def encore_une_fonction_d_exemple_inutile(l):
        return l1 + [4]

    Le paramètre est l et la variable utilisée est l1.

    Et sinon, c’est pas plutôt “Tout est possible. Tout est réalisable. C’est le jeu de la vie.”

  • Recher

    Il est vrai, merci.

    Par contre, j’ai à nouveau subi le plantage des caractères “>” dans les commentaires. (Tu m’avais donnée l’astuce pour ne pas le subir quand on écrit un article, mais pour les commentaires, je ne sais pas si il y a une solution).

    Suggestion qui tue :
    Nous avons déjà un magnifique Clippy, qui prodigue divers conseils pertinents au fur et à mesure de l’écriture d’un commentaire. Il pourrait être rendu encore plus utile qu’il ne l’est actuellement. Si il détecte un “>>>” dans une balise “pre”, il pourrait peut-être proposer un petit avertissement de ce style :

    Il semble que vous écriviez des caractères qui seront mal gérés par le plugin daubesque de wordpress. Ne voudriez-vous pas remplacer les “>>>” par des “»»” ?

  • Sam Post author

    Pas con. Et done.

    Et au passage, j’ai relaxé les règles de conversion de HTML pour les auteurs non admin. Peut être que c’était ça le problème ?

  • foxmask

    Tout est possible. Tout est imaginable. C’est le jeu de la vie.

    Chevalier et Laspalles sortez de ce corps :)

  • gloubidabu

    En plus de la coquille relevée par Kontre (l1) qui n’a pas été corrigée, on a :
    – le pourtour anale
    – La même problématique existe pour leS classes
    – s’apperçoive

  • matleg

    salut,

    merci pour cet article très clair,

    petite faute de frappe il me semble :

    dans la classe AuChocolat: suppéments –> supplements

  • FredWarning

    Juste une question sur l’exemple du début:

    a = [1,2,3]

    print(a)

    [1, 2, 3]

    a = b

    Traceback (most recent call last):

    File “”, line 1, in

    NameError: name ‘b’ is not defined

    Ne devrait-il pas être :

    b = a

    b.append(4)

    print(a)

    [1, 2, 3, 4]

    Merci à vous et comme d’hab, Très bon tuto!!!!

  • Jean-Baptiste

    Bonjour,

    Merci pour cet article. J’ai trouvé quelques coquilles :

    En Python, il existe en effet deux types d’objets: les mutables (listes, dictionnaires, sets, objets custo, etc) et les non nons mutables (strings, int, floats, tuples, etc).

    On ne peut pas modifier 152 une fois que l’objet int est créé. Mais on peut rajouter des éléments à une liste après qu’elle soit créée. Les ints sont non mutables. Les lists sont mutables.

    Vous ne modifiez pas la chaîne originale: vous créez créé une copie de la chaîne, et la chaîne de départ n’a pas bougé.

    on s’en oint le pourtour anal anale avec une pelle à tarte

    elle a des conséquences sur des objets qui existent existe en dehors d’elle même.

  • Maxime

    Salut !

    Merci pour tout le boulot ! Je suis retombé sur cette page parce que j’ai un stack overflow méchant dans mon programme python, et je me suis dit que la solution devait être quelques part ici :p . Mais du coup, je ne comprends pas trop pourquoi on devrait utiliser des fonctions sans effets de bord et quelle influence ça a sur la mémoire.

    Admettons que j’ai une liste d’objets Foo() et une liste d’objets Goo(), ces derniers pointent chacun vers une instance de la liste de Foo. Si je veux modifier cette liste de Foo tout en gardant les références des Goo vers les Foo, je suis bien obligé d’utiliser une fonction avec un effet de bord, non ?

    Je sais pas si j’ai été clair :D

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.