id(), None et bidouilleries mémoire en python. 21


Ceci est un post invité de Réchèr posté sous licence creative common 3.0 unported.

Mon maître-ninja python, entre deux riffs sur sa contrebasse électrique, m’avait un jour dit : “il ne faut pas écrire if a == None:, mais if a is None:“. Il m’avait ensuite donné une justification pertinente, que je n’ai pas retenue, car j’étais en train de penser à des nichons. Puis il avait conclu par “on n’est pas égal au vide. On EST le vide.”

Rien que pour vous, ainsi que pour m’auto-déculpabiliser de penser souvent à des nichons, j’ai parcouru l’internet entier à dos de souris et j’ai retrouvé la justification. Mais avant de vous la livrer, quelques explications préliminaires.

On n’a pas de pétrole, mais on a des id()

Et quand on n'a pas d'id, on a des fix !

La fonction id(), présente dans __builtins__, renvoie un entier, représentant l’identifiant interne de n’importe quel objet, quel que soit son type. Concrètement, il s’agit de l’adresse mémoire dans laquelle il est stocké. Jouons un peu avec.

»» a = 1
»» id(a)
19846928
»» b = "plop"
»» id(b)
33984800
»» id(2)
19846916

Si vous faites cela chez vous, vous n’obtiendrez certainement pas les mêmes nombres. Ce qui est important à repérer, c’est que dans ce premier exemple, tous les id sont différents. Au passage, vous remarquerez que même les constantes ont un id. Je ne sais pas exactement comment cela fonctionne en interne, mais je suppose que lorsqu’on écrit le chiffre 2, on oblige le python à stocker cette valeur dans une sorte de monde magique contenant tout ce qui a été utilisé depuis le début de l’exécution, même si certaines de ces valeurs ne sont plus référencées nul part.

Il est parfois intéressant de vérifier que deux id sont égaux ou différents. Mais vous ne pouvez pas faire grand chose de plus. Les caractères de la chaîne “plop” sont, à priori, stockés les uns à la suite des autres, dans une seule zone mémoire (tous les langages de programmation gèrent les strings de cette manière), mais vous ne pouvez pas mettre ce fait en évidence.

»» id(b[0])
19483368
»» id(b[1])
19989736
»» id(b[2])
19989760
»» id(b[0:2])
31983736
»» id(b[1:3])
31983424

Les adresses ne se suivent pas, et ne sont même pas rangées dans l’ordre croissant. Ce n’est pas vraiment exploitable. En revanche, il y a quelque chose de rigolo à constater :

»» c = "pouet"
»» d = "pouet"
»» id(c)
33984768
»» id(d)
33984768
»» id("pouet")
33984768

On a deux variables différentes, contenant la même chose, et elles pointent sur la même adresse mémoire. Mais alors, si je modifie c, ça va aussi modifier d ! Ohlala, Comment le python va faire ? Rassurez-vous, il s’en sort.

»» c += "a"
»» c
'poueta'
»» d
'pouet'
»» id(c)
33855904
»» id(d)
33984768

Le type string est immutable (ndm aussi: “immuable”). Cela signifie que vous ne pouvez rien changer dedans. Tout ce que vous pouvez faire, c’est réaffecter des variables (à une autre string, à n’importe quoi, …). Dans cet exemple, la réaffectation de c l’a fait pointer sur une autre zone de la mémoire. Mais sinon, la variable d n’a pas changé, ni son pointage, ni le contenu de ce qui est pointé. Ouf !!

Je te fais "pouet-pouet", tu me fais "pouet-pouet" ...

Au passage, je rappelle que toutes les fonctions de traitement de chaîne de caractères (replace(), upper(), strip(), …) ne font jamais la modification “sur place”. Elles renvoient la valeur sous forme d’une nouvelle string. Si vous voulez modifier sur place, vous êtes obligés de faire une réaffectation explicite. Quelque chose comme : c = c.upper()

Bidouilleries optimisatoires internes

Je sens que vous redemandez encore du jeu de mots pourri. So, here comze da None !

Le fait qu’un objet soit immutable n’oblige pas le python à créer une seule zone mémoire par valeur. Dans l’exemple précédent, lorsque j’ai affecté les variables c et d à “pouet”, leurs id auraient pu être différentes dès le départ, et les zones mémoires pointées auraient toutes les deux contenues “pouet”. Il se trouve que le python les a mises en commun, par souci d’optimisation. Cependant, je ne suis pas sûr que cette technique soit appliquée par tous les interpréteurs, toutes les versions, et dans tous les contextes. Mais “chez moi, ça marche”.

Cette optimisation est également effectuée pour les valeurs booléennes et les valeurs numériques entières. Mais pas pour les floats, alors qu’à ma connaissance, ils sont eux aussi immutables.

»» id(True)
505281316
»» id(True)
505281316
»» id(False)
505280988
»» id(False)
505280988
»» id(10)
19846820
»» id(10)
19846820
»» id(10.0)
19879104
»» id(10.0)
31338664
# Refaites des id(10.0), et vous aurez encore d'autres valeurs différentes.
# Mais peut-être pas à chaque fois. On ne peut pas le prévoir.

En revanche, l’optimisation de la valeur None est garantie, quel que soit votre python et son contexte. Il n’y a toujours qu’une et une seule instance de None dans une exécution de python. Je n’ai pas trouvé d’annonce officielle concernant cette garantie, mais ça se dit un peu partout sur les forums. (Et si c’est sur internet, c’est forcément vrai).

Jouons un peu avec la singletonitude du None.

»» id(None)
505255972
»» z = None
»» id(z)
505255972
»» zzzz = [None, ] * 30
»» zzzz
[None, None, None, None, None, None, None, None, None, None, None, None, None,
None, None, None, None, None, None, None, None, None, None, None, None, None,
None, None, None, None]
»» [ id(elem) for elem in zzzz ]
[505255972, 505255972, 505255972, 505255972, 505255972, 505255972, 505255972,
505255972, 505255972, 505255972, 505255972, 505255972, 505255972, 505255972,
505255972, 505255972, 505255972, 505255972, 505255972, 505255972, 505255972,
505255972, 505255972, 505255972, 505255972, 505255972, 505255972, 505255972,
505255972, 505255972]

Disgression : les mot-clés True et False peuvent être réaffectés. Mais pas le mot-clé None. Mais je m’égare. Revenons à nos agneaux, comme dirait Hannibal Lecter.

noir is noir. (Il n’y a plus d’ayspouuaaaarr).

"Mmmmppff... psshhhh..." (#000000 Vador)

Laissez-moi maintenant vous introduire (schlorrp) l’opérateur is. Il est présent nativement dans le python, et permet de comparer les id de deux objets. A is B renvoie un booléen, le même que celui que renverrait id(A) == id(B).

A quoi cela pourrait-il donc servir ? Eh bien … De même que la fonction id ne sert pas à grand chose, is est, lui aussi, d’une utilité discutable. Mais attendez, ne partez pas tout de suite.

L’opérateur is est vraiment tout simple. Il ne peut pas être surchargé, et fait toujours la même chose quel que soit les opérandes : une comparaison d’id.

L’opérateur == a l’air tout bête, vu comme ça, mais il peut être surchargé, et il effectue une suite d’actions pas forcément triviales. Il appelle la fonction __eq__ si elle existe, sinon il essaie avec la fonction __cmp__, et peut-être encore d’autres choses que je ne détaillerai pas ici, parce que je ne me suis pas renseigné en détail sur le sujet. (Faut que je conserve une partie de mon temps pour le pensage à des nichons, vous comprenez bien).

En règle générale, dès que c’est possible, il vaut mieux utiliser un truc simple plutôt qu’un truc compliqué. Malheureusement, comme on ne maîtrise pas vraiment la façon dont les id sont gérés et optimisés, l’utilisation du is serait trop risquée. Sauf dans un cas bien identifié. Devinez lequel ?

Le None, qui n’a toujours qu’une et une seule id ! Joie ! Noël !

Au fait, pendant qu’on y est, ne pourrait-on pas utiliser is avec les valeurs True et False ? A priori, ces deux valeurs sont gérées de la même manière que le None ?

Certes, mais pour les booléens, on a encore plus simple. Au lieu de tester if A is True: ou if A == True:, on peut directement tester if A:. Pour False, on teste if not A:. Voili voilà.

Le béni oui-oui

Un béni oui-oui !

Pour finir, une petite justification supplémentaire, que je n’ai jamais rencontré dans la vraie vie, mais on va faire comme si.

Alors voilà, vous êtes quelqu’un de bizarre, et vous avez besoin d’une classe qui est égale à tout. Donc vous surchargez la fonction __eq__.

»» class BeniOuiOui:
    def __eq__(self, other):
        # Je suis une classe magique, qui est égale à tout ce qui existe !
        return True
»» beniOuiOui = BeniOuiOui()

Un peu plus loin dans le code, vous avez une variable, récupérée d’on ne sait trop où, et vous voulez savoir si c’est None, ou si c’est une instance de béni-oui-oui. Pour faire cela, vous êtes obligés d’utiliser is, car == vous répondrait systématiquement True.

»» beniOuiOui == 2
True
»» beniOuiOui == "n'importe quoi"
True
»» beniOuiOui == None
True
»» beniOuiOui is None
False

this (is not) une pipe

Ce serait même plutôt le contraire.

Comment fait-on pour vérifier que quelque chose n’est pas None ? La première réponse qui vient à l’esprit, c’est not A is None. Mais on peut aussi utiliser A is not None. C’est chouette, ça ressemble à du langage naturel.

Oh, mais, attendez … Est-ce que cette écriture n’est pas un peu dangereuse ?

»» not None
True

not None renvoie True. Pourquoi pas. C’est ce qui semble le plus logique.

»» 1 is True
False

1 is True est faux. Ca semble correct aussi. La valeur numérique 1 et la valeur booléenne True sont égales, mais ce ne SONT pas les mêmes objets.

Mais alors, si j’écris 1 is not None, le python va transformer le not None en True, ça va faire 1 is True, et ça va renvoyer faux. Ce n’est pas ce que je veux ! La valeur numérique 1, ce n’est pas None. Moi je veux que 1 is not None me renvoie vrai.

Arrggh ! Pleurons mes amis ! Et regardons avec horreur le python se désintégrer tout seul dans une bouffée de logique !

»» 1 is not None
True

Ah tiens non. Hmmm… Attendez voir…

»» 1 is (not None)
False

Qu’est-ce donc que cela ? Figurez-vous que is not est un opérateur. Il compare les id, et renvoie True s’ils sont différents. Je vous laisse confirmer cela en consultant la doc du python, ou en faisant un petit help("is") dans la console.

Voilà, j’espère que ça vous a plu. La prochaine fois, nous essaierons de déceler dans le langage Brainfuck des éléments de la thèse existentialiste selon Jean-Paul Sartre.


About recher

Ouvrier-codeur dans une start-down experte en solution de karmagraphie logicielle. Inventeur d'un langage de programmation permettant de coder en alsacien. Ardent défenseur du mot "chevals". Membre du bar (mais pas du foo).

21 thoughts on “id(), None et bidouilleries mémoire en python.

  • Max

    la Classe à Dallas la derniere photo :)

    Merci pour la contribution ! Je surkiffe les clients…heu patients de ce blog…

  • Xavier Combelle

    Il n’y a toujours qu’une et une seule instance de None dans une exécution de python. Je n’ai pas trouvé d’annonce officielle concernant cette garantie, mais ça se dit un peu partout sur les forums.

    Je n’ai pas trouvé d’endroit ou c’était spécifié mais dans http://docs.python.org/3/c-api/none.html il est dit Since None is a singleton, testing for object identity (using == in C) is sufficient

  • Soli

    J’avoue, j’ai passé l’intégralité de l’article à chercher le lien entre nichons et pot de yaourt… J’ai bien une idée, mais bon…

  • Recher

    @Xavier : Merci pour le lien ! Vu que ça vient du site officiel de python, ça constitue presque la garantie officielle de la singletonnité Noniale.

    @Soli : il n’y a pas de lien direct entre le yaourt et les nichons. Mais il y a en a un entre le yaourt et le sujet de l’article. Pour le découvrir, laisse ton curseur de souris sur l’image, et attends quelques secondes !

    C’est mon premier article de contribuage. Je dois reconnaître que j’en suis assez fier. J’essayerais d’en faire d’autres à l’occasion. En attendant, enjoy !

    @Clippy : Oui, j’ai écrit “yaourt”, et alors ! Je vois pas en quoi c’est graveleux !

  • Max

    @soli, ce qu’il fait à l’intérieur se voit à l’extérieur :)

  • JeromeJ

    Chouette contrib :) merci j’aime le ton rédactoriel (proche du fidèle ton rédactoriel de ce blog … cool !)

  • Recher

    Oui je suis un grand fan des jeux de mots honteux. Ca désespère mes collègues et mes amis.

    Je suis une sorte de Raymond Devos Discount.

  • Max

    @Recher j’adore les jeux de mots foireux, surtout quand ils sont rapportés à des films qui datent pas mal… Avec un pote dans des conversions faut avoir vu tous les nanards des années 80 pour nous suivre :)
    Pour vraiment s’en rendre compte faudrait une lecture plus profonde

  • Etienne

    Chouette article! C’est fou ce qu’il y a dire sur Rien. Tiens, pour le coup ça me fait penser à je sais plus quel auteur (Terry Pratchett ou Ursula le Guin) qui écrivait: “le noir n’est pas une couleur, c’est l’absence de couleur”. Dans le même genre, on pourrait dire: “le vide n’est pas une chose, c’est l’absence de choses”. Si rien n’est pas quelque chose, pas étonnant qu’il n’y en ait qu’un… Enfin, je me comprends.

    Ps:
    Merci pour l’id

  • Sam

    On a aucune variable pour l’octarine par contre. C’est frustrant.

  • Feth

    Article utile dont je goûte parfois l’humour… n’empêche, je vais jouer les prudes et passer pour le pénible de service sur ce blog utile.
    Je pense que l’affichage de la présente page devrait être déconseillé aux publics qui ne souhaitent pas voir certaines images.
    Au moins un avertissement, quoi. Est-ce que vous vous serviriez de cet article pour enseigner les subtilités de Python à un gamin ou à quelqu’un que vous ne connaissez pas (imaginez, si vous êtes de cultures différentes) ?

  • Sam

    Oui. Je le fais d’ailleurs régulièrement. J’ai même pris cher pour l’avoir fais dans un pays à majorité musulman.

    La tag line du blog est “Python, Django, Git et du cul”

    Si ce genre de choses vous choque ou vous pose problème, je peux le comprendre, mais la seule solution est de ne pas lire sam et max.

    C’est comme south park. C’est un dessin animé. C’est plein de philo, d’analyse politique, qui mériteraient d’être consulté en dehors du contexte de la série. Mais la série est trash et le restera. Soit on prend tout le contenu, soit on ne regarde pas South park.

  • Feth

    Sam: en effet, j’avais oublié la ‘tagline’. Vu le succès mérité de vos pages, j’y vais comme sur le web plat, le plus souvent en suivant des liens. Faites comme vous voulez, comme vous avez le temps de faire, moi je voudrais un avertissement (mais je ne l’exige pas, hein).

  • Feth

    @Max un qui permette d’aller à la piscine, faire du cheval, bref, mener une vie normale tous les jours du mois. /o\

  • Siltaar

    J’ai envie de dire (même 5 ans plus tard) : merci à Feth pour cette belle preuve d’ouverture d’esprit :-) Et merci à Sam et Max, de défendre quelques belles notions de liberté d’expression :) (de faire de l’internet à l’ancienne quoi…)

    Ensuite, dans le futur donc, les floats sont enfin optimisés :

    id(10.0)

    19715744

    id(10.0)

    19715744

    Au moins avec (mais sûrement depuis avant) :

    – ‘2.7.8 (2.4.0+dfsg-3, Dec 20 2014, 13:30:46)\n[PyPy 2.4.0 with GCC 4.9.2]’

    – ‘2.7.9 (default, Jun 29 2016, 13:08:31) \n[GCC 4.9.2]’

    – ‘3.4.2 (default, Oct 8 2014, 10:45:20) \n[GCC 4.9.1]’

    D’ailleurs, avec pypy, le id sont tout chelou :

    id(10.0)

    36974552940711772165L

    id(10.0)

    36974552940711772165L

Leave a comment

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <pre> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

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