Pourquoi les éléments suivants se comportent-ils de manière inattendue en Python?
>>> a = 256
>>> b = 256
>>> a is b
True # This is an expected result
>>> a = 257
>>> b = 257
>>> a is b
False # What happened here? Why is this False?
>>> 257 is 257
True # Yet the literal numbers compare properly
J'utilise Python 2.5.2. En essayant différentes versions de Python, il semble que Python 2.3.3 montre le comportement ci-dessus entre 99 et 100.
Sur la base de ce qui précède, je peux émettre l’hypothèse que Python est implémenté de manière interne, de sorte que les "petits" entiers sont stockés de manière différente des entiers plus grands et que l’opérateur is
peut faire la différence. Pourquoi cette abstraction qui fuit? Quel est le meilleur moyen de comparer deux objets arbitraires pour voir s’ils sont identiques quand je ne sais pas à l’avance s’ils sont numériques ou non?
Regarde ça:
>>> a = 256
>>> b = 256
>>> id(a)
9987148
>>> id(b)
9987148
>>> a = 257
>>> b = 257
>>> id(a)
11662816
>>> id(b)
11662828
EDIT: Voici ce que j'ai trouvé dans la Python 2, "Objets entiers simples" (Il en va de même pour Python ):
L'implémentation actuelle conserve un tableau d'objets entiers pour tous les entiers compris entre -5 et 256; lorsque vous créez un entier dans cette plage, vous récupérez simplement une référence à l'objet existant. Donc, il devrait être possible de changer la valeur de 1. Je suppose que le comportement de Python dans ce cas n'est pas défini. :-)
L'opérateur "is" de Python se comporte de manière inattendue avec des entiers?
En résumé - permettez-moi de souligner: N'utilisez pas is
pour comparer des nombres entiers.
Ce n'est pas un comportement sur lequel vous devriez avoir des attentes.
À la place, utilisez ==
Et !=
Pour comparer l'égalité et l'inégalité, respectivement. Par exemple:
>>> a = 1000
>>> a == 1000 # Test integers like this,
True
>>> a != 5000 # or this!
True
>>> a is 1000 # Don't do this! - Don't use `is` to test integers!!
False
Pour le savoir, vous devez connaître les éléments suivants.
Tout d'abord, que fait is
? C'est un opérateur de comparaison. De la documentation :
Les opérateurs
is
etis not
Testent l'identité de l'objet:x is y
Est vraie si et seulement si x et y sont le même objet.x is not y
Donne la valeur de vérité inverse.
Et si ce qui suit est équivalent.
>>> a is b
>>> id(a) == id(b)
De la documentation :
id
Renvoie "l'identité" d'un objet. C'est un entier (ou un entier long) qui est garanti d'être unique et constant pour cet objet pendant sa durée de vie. Deux objets dont la durée de vie ne se chevauchent pas peuvent avoir la même valeurid()
.
Notez que le fait que l'identifiant d'un objet dans CPython (l'implémentation de référence de Python) soit l'emplacement en mémoire est un détail d'implémentation. D'autres implémentations de Python (comme Jython ou IronPython) pourraient facilement avoir une implémentation différente pour id
.
Alors, quel est le cas d'utilisation de is
? décrit par PEP8 :
Les comparaisons avec des singletons comme
None
devraient toujours être effectuées avecis
ouis not
, Jamais avec les opérateurs d'égalité.
Vous posez et énoncez la question suivante (avec code):
Pourquoi les éléments suivants se comportent-ils de manière inattendue en Python?
>>> a = 256 >>> b = 256 >>> a is b True # This is an expected result
C'est pas un résultat attendu. Pourquoi est-ce attendu? Cela signifie seulement que les entiers évalués à 256
Référencés par a
et b
sont la même instance d'entier. Les entiers sont immuables en Python, ils ne peuvent donc pas changer. Cela ne devrait avoir aucun impact sur le code. Il ne faut pas s'y attendre. C'est simplement un détail de mise en œuvre.
Mais peut-être devrions-nous nous réjouir qu’il n’y ait pas de nouvelle instance distincte en mémoire chaque fois que nous déclarons une valeur égale à 256.
>>> a = 257 >>> b = 257 >>> a is b False # What happened here? Why is this False?
On dirait que nous avons maintenant deux instances distinctes d'entiers avec la valeur de 257
En mémoire. Puisque les entiers sont immuables, cela gaspille de la mémoire. Espérons que nous n'en gaspillons pas beaucoup. Nous ne sommes probablement pas. Mais ce comportement n'est pas garanti.
>>> 257 is 257 True # Yet the literal numbers compare properly
Eh bien, cela ressemble à votre implémentation particulière de Python essaie d’être intelligent et de ne pas créer d’entiers de valeur redondante en mémoire, sauf obligation. Vous semblez indiquer que vous utilisez l’implémentation référent de Python, qui est CPython. Bon pour CPython.
Ce serait peut-être encore mieux si CPython pouvait le faire globalement, s'il pouvait le faire à moindre coût (car il y aurait un coût à la recherche), peut-être une autre implémentation.
Mais en ce qui concerne l'impact sur le code, vous ne devez pas vous soucier de savoir si un entier est une instance particulière d'un entier. Vous ne devez vous soucier que de la valeur de cette instance et vous utiliserez les opérateurs de comparaison normaux pour cela, c'est-à-dire ==
.
is
is
vérifie que le id
de deux objets est identique. Dans CPython, id
est l'emplacement en mémoire, mais il pourrait s'agir d'un autre numéro d'identification unique dans une autre implémentation. Pour reformuler ceci avec le code:
>>> a is b
est le même que
>>> id(a) == id(b)
is
alors?Cela peut être une vérification très rapide par rapport à dire, vérifiant si deux très longues chaînes ont la même valeur. Mais puisque cela s'applique à l'unicité de l'objet, nous avons donc des cas d'utilisation limités pour celui-ci. En fait, nous voulons surtout l'utiliser pour vérifier None
, qui est un singleton (une seule instance existant à un endroit de la mémoire). Nous pourrions créer d'autres singletons s'il est possible de les mélanger, ce que nous pourrions vérifier avec is
, mais ceux-ci sont relativement rares. Voici un exemple (fonctionnera dans Python 2 et 3) par exemple.
SENTINEL_SINGLETON = object() # this will only be created one time.
def foo(keyword_argument=None):
if keyword_argument is None:
print('no argument given to foo')
bar()
bar(keyword_argument)
bar('baz')
def bar(keyword_argument=SENTINEL_SINGLETON):
# SENTINEL_SINGLETON tells us if we were not passed anything
# as None is a legitimate potential argument we could get.
if keyword_argument is SENTINEL_SINGLETON:
print('no argument given to bar')
else:
print('argument to bar: {0}'.format(keyword_argument))
foo()
Quelles impressions:
no argument given to foo
no argument given to bar
argument to bar: None
argument to bar: baz
Nous voyons donc que, avec is
et une sentinelle, nous sommes en mesure de faire la distinction entre le moment où bar
est appelé sans arguments et le moment où il est appelé avec None
. Ce sont les principaux cas d’utilisation pour is
- do pas l’utiliser pour tester l’égalité des entiers, des chaînes, des n-uplets ou d’autres choses du même genre.
Cela dépend si vous cherchez à voir si 2 choses sont égales ou le même objet.
is
vérifie s'ils sont le même objet, pas seulement égaux. Les petites ints pointent probablement vers le même emplacement mémoire pour une utilisation plus efficace de l'espace
In [29]: a = 3
In [30]: b = 3
In [31]: id(a)
Out[31]: 500729144
In [32]: id(b)
Out[32]: 500729144
Tu devrais utiliser ==
pour comparer l'égalité d'objets arbitraires. Vous pouvez spécifier le comportement avec le __eq__
, et __ne__
les attributs.
Je suis en retard mais vous voulez une source avec votre réponse? *
La bonne chose à propos de CPython est que vous pouvez réellement voir la source pour cela. Je vais utiliser des liens pour la version 3.5
Pour le moment; trouver les 2.x
correspondants est trivial.
Dans CPython, la fonction C-API
Qui gère la création d'un nouvel objet int
est PyLong_FromLong(long v)
. La description de cette fonction est:
L'implémentation actuelle conserve un tableau d'objets entiers pour tous les entiers compris entre -5 et 256. Lorsque vous créez un int dans cette plage, vous récupérez simplement une référence à l'objet existant . Donc, il devrait être possible de changer la valeur de 1. Je suppose que le comportement de Python dans ce cas n'est pas défini. :-)
Je ne sais pas pour vous mais je vois cela et je pense: Trouvons ce tableau!
Si vous n'avez pas manipulé le code C
implémentant CPython , vous devriez , tout est très bien organisé et lisible. Dans notre cas, nous devons chercher dans le sous-répertoire Objects/
de l'arborescence du répertoire du code source principal .
PyLong_FromLong
Traite des objets long
, il ne devrait donc pas être difficile d'en déduire qu'il faut jeter un coup d'œil à l'intérieur longobject.c
. Après avoir regardé à l'intérieur, vous pourriez penser que les choses sont chaotiques; ils le sont, mais ne craignez pas, la fonction que nous recherchons est en train de refroidir line 230
nous attend pour le vérifier. C'est une fonction assez petite pour que le corps principal (à l'exception des déclarations) soit facilement collé ici:
PyObject *
PyLong_FromLong(long ival)
{
// omitting declarations
CHECK_SMALL_INT(ival);
if (ival < 0) {
/* negate: cant write this as abs_ival = -ival since that
invokes undefined behaviour when ival is LONG_MIN */
abs_ival = 0U-(unsigned long)ival;
sign = -1;
}
else {
abs_ival = (unsigned long)ival;
}
/* Fast path for single-digit ints */
if (!(abs_ival >> PyLong_SHIFT)) {
v = _PyLong_New(1);
if (v) {
Py_SIZE(v) = sign;
v->ob_digit[0] = Py_SAFE_DOWNCAST(
abs_ival, unsigned long, digit);
}
return (PyObject*)v;
}
Maintenant, nous ne sommes pas C
maître-code-haxxorz mais nous ne sommes pas non plus stupides, nous pouvons voir que CHECK_SMALL_INT(ival);
nous regardant tous de manière séduisante; nous pouvons comprendre que cela a quelque chose à voir avec cela. Voyons ça:
#define CHECK_SMALL_INT(ival) \
do if (-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS) { \
return get_small_int((sdigit)ival); \
} while(0)
C'est donc une macro qui appelle la fonction get_small_int
Si la valeur ival
satisfait à la condition:
if (-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS)
Alors que sont NSMALLNEGINTS
et NSMALLPOSINTS
? Si vous avez deviné les macros, vous n'obtenez rien car ce n'était pas une question si difficile. en tout cas, les voici:
#ifndef NSMALLPOSINTS
#define NSMALLPOSINTS 257
#endif
#ifndef NSMALLNEGINTS
#define NSMALLNEGINTS 5
#endif
Donc, notre condition est if (-5 <= ival && ival < 257)
appeler get_small_int
.
Aucun autre endroit où aller mais continuez notre voyage en regardant get_small_int
Dans toute sa splendeur (eh bien, regardons simplement son corps, car ce sont là des choses intéressantes):
PyObject *v;
assert(-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS);
v = (PyObject *)&small_ints[ival + NSMALLNEGINTS];
Py_INCREF(v);
D'accord, déclarez un PyObject
, affirmez que la condition précédente est vérifiée et exécutez l'affectation:
v = (PyObject *)&small_ints[ival + NSMALLNEGINTS];
small_ints
Ressemble beaucoup à ce tableau que nous recherchions .. et ça l'est! Nous aurions pu lire la fichue documentation et nous le saurions depuis le début!:
/* Small integers are preallocated in this array so that they
can be shared.
The integers that are preallocated are those in the range
-NSMALLNEGINTS (inclusive) to NSMALLPOSINTS (not inclusive).
*/
static PyLongObject small_ints[NSMALLNEGINTS + NSMALLPOSINTS];
Alors oui, c'est notre gars. Lorsque vous souhaitez créer un nouveau int
dans la plage [NSMALLNEGINTS, NSMALLPOSINTS)
, Vous récupérez simplement une référence à un objet déjà préalloué.
Puisque la référence fait référence au même objet, émettre id()
directement ou vérifier l'identité avec is
dessus renverra exactement la même chose.
Lors de l'initialisation dans _PyLong_Init
Python entrera volontiers dans une boucle for, faites-le pour vous:
for (ival = -NSMALLNEGINTS; ival < NSMALLPOSINTS; ival++, v++) {
// Look me up!
}
J'espère que mon explication vous a clairement expliqué les choses C
.
C'est en fait plus facile à expliquer, et j'ai déjà essayé de le faire ; c'est dû au fait que Python exécutera cette déclaration interactive:
>>> 257 is 257
comme un seul bloc. Lors de la complétion de cette instruction, CPython verra que vous avez deux littéraux correspondants et utilisera le même PyLongObject
représentant 257
. Vous pouvez voir cela si vous faites la compilation vous-même et examinez son contenu:
>>> codeObj = compile("257 is 257", "blah!", "exec")
>>> codeObj.co_consts
(257, None)
Lorsque CPython effectue l'opération; il va maintenant juste charger exactement le même objet:
>>> import dis
>>> dis.dis(codeObj)
1 0 LOAD_CONST 0 (257) # dis
3 LOAD_CONST 0 (257) # dis again
6 COMPARE_OP 8 (is)
Ainsi, is
renverra True
.
* - Je vais essayer de formuler cela d’une manière plus introductive afin que la plupart puissent suivre.
Comme vous pouvez le vérifier fichier source intobject.c , Python met en cache de petits entiers pour plus d'efficacité. Chaque fois que vous créez une référence à un petit entier, vous faites référence au petit entier mis en cache, et non à un nouvel objet. 257 n'est pas un petit entier, il est donc calculé comme un objet différent.
Il vaut mieux utiliser ==
Dans ce but.
Je pense que vos hypothèses sont correctes. Expérimentez avec id
(identité de l'objet):
In [1]: id(255)
Out[1]: 146349024
In [2]: id(255)
Out[2]: 146349024
In [3]: id(257)
Out[3]: 146802752
In [4]: id(257)
Out[4]: 148993740
In [5]: a=255
In [6]: b=255
In [7]: c=257
In [8]: d=257
In [9]: id(a), id(b), id(c), id(d)
Out[9]: (146349024, 146349024, 146783024, 146804020)
Il semble que les nombres <= 255
sont traités comme des littéraux et tout ce qui précède est traité différemment!
Pour les objets de valeur immuables, tels que ints, chaînes ou datetime, l'identité de l'objet n'est pas particulièrement utile. Mieux vaut penser à l'égalité. L'identité est essentiellement un détail d'implémentation pour les objets de valeur. Comme ils sont immuables, il n'y a pas de différence réelle entre avoir plusieurs références sur le même objet ou plusieurs objets.
is
est l'opérateur d'opportunité d'identité (fonctionnant comme id(a) == id(b)
); c'est juste que deux nombres égaux ne sont pas nécessairement le même objet. Pour des raisons de performances, il se trouve que certains petits entiers sont mémoisés et ont donc tendance à être les mêmes (cela peut être fait car ils sont immuables).
PHP's L'opérateur ===
, Quant à lui, décrit la vérification de l'égalité et le type: x == y and type(x) == type(y)
selon le commentaire de Paulo Freitas. Cela suffira pour les nombres communs, mais différera de is
pour les classes qui définissent __eq__
De manière absurde:
class Unequal:
def __eq__(self, other):
return False
Apparemment, PHP permet la même chose pour les classes "intégrées" (ce que je veux dire par implémentées au niveau C, pas en PHP). Une utilisation légèrement moins absurde pourrait consister en un objet timer, qui a une valeur différente chaque fois qu'il est utilisé comme nombre. Pourquoi vouloir émuler Now
de Visual Basic au lieu de montrer qu'il s'agit d'une évaluation avec time.time()
je ne sais pas.
Greg Hewgill (OP) a formulé un commentaire de clarification: "Mon objectif est de comparer l'identité de l'objet plutôt que l'égalité de la valeur. Sauf pour les nombres, je veux traiter l'identité de l'objet de la même manière que l'égalité de la valeur."
Cela aurait encore une autre réponse, car nous devons classer les choses en chiffres ou non, pour choisir si nous comparons avec ==
Ou is
. CPython définit le protocole de numéro , y compris PyNumber_Check, mais ceci n'est pas accessible à partir de Python lui-même.
Nous pourrions essayer d'utiliser isinstance
avec tous les types de nombres connus, mais cela serait inévitablement incomplet. Le module types contient une liste StringTypes mais pas NumberTypes. Depuis Python 2.6, les classes de nombres intégrées ont une classe de base numbers.Number
, mais le problème est le même:
import numpy, numbers
assert not issubclass(numpy.int16,numbers.Number)
assert issubclass(int,numbers.Number)
À propos, NumPy produira des instances séparées de nombres faibles.
Je ne connais pas réellement de réponse à cette variante de la question. J'imagine que l'on pourrait théoriquement utiliser ctypes pour appeler PyNumber_Check
, Mais même cette fonction a été débattue , et ce n'est certainement pas portable. Nous devrons juste être moins précis sur ce que nous testons pour le moment.
En fin de compte, ce problème provient de Python ne possédant pas initialement une arborescence de types avec des prédicats tels que Scheme'snumber?
, Ou Haskell's = type classNum . is
vérifie l'identité de l'objet et non l'égalité des valeurs. PHP a également un historique coloré, où ===
se comporte apparemment comme is
uniquement sur des objets en PHP5, mais pas en PHP4 .
Il y a un autre problème qui n'est mentionné dans aucune des réponses existantes. Python est autorisé à fusionner deux valeurs immuables, et les petites valeurs int pré-créées ne sont pas le seul moyen d'y parvenir. Une implémentation de Python n'est jamais garanti de le faire, mais ils le font tous pour plus que de petites ints.
D’une part, il existe d’autres valeurs pré-créées, telles que les valeurs vide Tuple
, str
et bytes
, et quelques chaînes courtes (dans CPython 3.6, c’est le 256 chaînes de caractères latino-1 à caractère unique). Par exemple:
>>> a = ()
>>> b = ()
>>> a is b
True
Mais aussi, même les valeurs non pré-créées peuvent être identiques. Considérez ces exemples:
>>> c = 257
>>> d = 257
>>> c is d
False
>>> e, f = 258, 258
>>> e is f
True
Et cela ne se limite pas à int
valeurs:
>>> g, h = 42.23e100, 42.23e100
>>> g is h
True
De toute évidence, CPython ne vient pas avec une valeur float
pré-créée pour 42.23e100
. Alors, qu'est-ce qui se passe ici?
Le compilateur CPython fusionnera les valeurs constantes de certains types connus tels que int
, float
, str
, bytes
, dans la même unité de compilation. Pour un module, l'ensemble du module est une unité de compilation, mais dans l'interpréteur interactif, chaque instruction est une unité de compilation distincte. Puisque c
et d
sont définis dans des instructions distinctes, leurs valeurs ne sont pas fusionnées. Puisque e
et f
sont définis dans la même instruction, leurs valeurs sont fusionnées.
Vous pouvez voir ce qui se passe en désassemblant le bytecode. Essayez de définir une fonction qui fait e, f = 128, 128
Puis d'appeler dis.dis
Dessus et vous verrez qu'il y a une seule valeur constante (128, 128)
>>> def f(): i, j = 258, 258
>>> dis.dis(f)
1 0 LOAD_CONST 2 ((128, 128))
2 UNPACK_SEQUENCE 2
4 STORE_FAST 0 (i)
6 STORE_FAST 1 (j)
8 LOAD_CONST 0 (None)
10 RETURN_VALUE
>>> f.__code__.co_consts
(None, 128, (128, 128))
>>> id(f.__code__.co_consts[1], f.__code__.co_consts[2][0], f.__code__.co_consts[2][1])
4305296480, 4305296480, 4305296480
Vous remarquerez peut-être que le compilateur a stocké 128
En tant que constante même s'il n'est pas réellement utilisé par le bytecode, ce qui vous donne une idée du peu d'optimisation du compilateur de CPython. Ce qui signifie que les n-uplets (non vides) ne sont pas fusionnés:
>>> k, l = (1, 2), (1, 2)
>>> k is l
False
Mettez cela dans une fonction, dis
it, et regardez le co_consts
- il y a un 1
Et un 2
, Deux (1, 2)
qui partagent les mêmes 1
et 2
mais ne sont pas identiques et un tuple ((1, 2), (1, 2))
qui comporte deux nuplets égaux distincts.
CPython effectue une autre optimisation: l’internalisation des chaînes. Contrairement au pliage constant du compilateur, ceci n'est pas limité aux littéraux de code source:
>>> m = 'abc'
>>> n = 'abc'
>>> m is n
True
D'autre part, il est limité au type str
et aux chaînes de type type de stockage interne "compact compact", "compact" ou "prêt pour le legs" , et dans de nombreux autres cas. cas seulement "ascii compact" sera interné.
En tout état de cause, les règles relatives aux valeurs qui doivent être, peuvent être ou ne peuvent pas être distinctes varient d’une mise en œuvre à l’autre, d’une version à l’autre, et peut-être même d’une exécution du même code à une copie identique de la même implémentation. .
Il peut être intéressant d’apprendre les règles d’un seul Python pour le plaisir. Cependant, il ne vaut pas la peine de les utiliser dans votre code. La seule règle sûre est la suivante:
Ou, en d'autres termes, utilisez uniquement is
pour tester les singletons documentés (comme None
) ou créés uniquement à un emplacement du code (comme le _sentinel = object()
idiome).
Cela arrive aussi avec des chaînes:
>>> s = b = 'somestr'
>>> s == b, s is b, id(s), id(b)
(True, True, 4555519392, 4555519392)
Maintenant, tout semble aller bien.
>>> s = 'somestr'
>>> b = 'somestr'
>>> s == b, s is b, id(s), id(b)
(True, True, 4555519392, 4555519392)
C'est prévu aussi.
>>> s1 = b1 = 'somestrdaasd ad ad asd as dasddsg,dlfg ,;dflg, dfg a'
>>> s1 == b1, s1 is b1, id(s1), id(b1)
(True, True, 4555308080, 4555308080)
>>> s1 = 'somestrdaasd ad ad asd as dasddsg,dlfg ,;dflg, dfg a'
>>> b1 = 'somestrdaasd ad ad asd as dasddsg,dlfg ,;dflg, dfg a'
>>> s1 == b1, s1 is b1, id(s1), id(b1)
(True, False, 4555308176, 4555308272)
Maintenant c'est inattendu.
Jetez un oeil ici
L'implémentation actuelle conserve un tableau d'objets entiers pour tous les entiers compris entre -5 et 256; lorsque vous créez un entier dans cette plage, vous récupérez simplement une référence à l'objet existant.
Python 3.8: https://docs.python.org/3.8/whatsnew/3.8.html#changes-in-python-behavior
Le compilateur produit maintenant un SyntaxWarning lorsque les contrôles d’identité (utilisés ou non) sont utilisés avec certains types de littéraux (par exemple des chaînes, des entiers). Celles-ci peuvent souvent fonctionner par accident dans CPython, mais elles ne sont pas garanties par la spécification du langage. L'avertissement conseille aux utilisateurs d'utiliser des tests d'égalité (== et! =) À la place. (Contribution de Serhiy Storchaka dans bpo-34850.)