web-dev-qa-db-fra.com

L'opérateur "is" se comporte de manière inattendue avec des entiers

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?

463
Greg Hewgill

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. :-)

356
Cybis

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

Explication

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 et is 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 valeur id().

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 avec is ou is not, Jamais avec les opérateurs d'égalité.

La question

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 ==.

Que fait 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)

Pourquoi voudrions-nous utiliser 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.

95
Aaron Hall

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.

56
JimB

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.

Mais quand sont-ils alloués?

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.


Mais, 257 est 257? Quoi de neuf?

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.

44

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.

36
Angel

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!

19
Amit

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.

12
babbageclunk

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 .

8
Yann Vernier

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:

  • N'écrivez pas de code qui suppose deux valeurs immuables égales mais créées séparément, identiques.
  • N'écrivez pas de code supposant que deux valeurs immuables égales mais créées séparément sont distinctes.

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).

7
abarnert

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.

4
sobolevn

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.

3
user5319825

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.)

0
cclauss