is – Sam & Max http://sametmax.com Du code, du cul Tue, 10 Sep 2019 09:14:50 +0000 en-US hourly 1 https://wordpress.org/?v=4.9.7 32490438 id(), None et bidouilleries mémoire en python. http://sametmax.com/id-none-et-bidouilleries-memoire-en-python/ http://sametmax.com/id-none-et-bidouilleries-memoire-en-python/#comments Sat, 10 Nov 2012 17:37:54 +0000 http://sametmax.com/?p=2947

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.

]]>
http://sametmax.com/id-none-et-bidouilleries-memoire-en-python/feed/ 21 2947