Question

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 apparaît que Python 2.3.3 présente le comportement ci-dessus entre 99 et 100.

Sur la base de ce qui précède, je peux émettre une hypothèse selon laquelle Python est implémenté de manière interne de telle sorte que "petit" les nombres entiers sont stockés d'une manière différente des nombres entiers plus grands et l'opérateur est 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?

Était-ce utile?

La solution

Regardez ceci:

>>> 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 documentation Python 2, & Plain; Entier Objets " (il en va de même pour Python 3 ):

  

L’implémentation actuelle conserve une   tableau d'objets entiers pour tous   entiers compris entre -5 et 256, lorsque vous   créer un int dans cette gamme vous   en fait juste récupérer une référence à   l'objet existant. Donc ça devrait être   possible de changer la valeur de 1. Je   soupçonner le comportement de Python   ce cas n'est pas défini. : -)

Autres conseils

  

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

En résumé, soulignons: N'utilisez pas is pour comparer des entiers.

Ce n'est pas un comportement pour lequel vous devriez avoir des attentes.

Utilisez plutôt == et ! = = / / code> pour comparer, respectivement, l'égalité et l'inégalité. 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 ? C'est un opérateur de comparaison. De la documentation :

  

L'opérateur est et n'est pas à tester pour l'identité de l'objet: x est y y est vrai   si et seulement si x et y sont le même objet. x n'est pas y donne le   valeur de vérité inverse.

Et les éléments suivants sont équivalents.

>>> a is b
>>> id(a) == id(b)

De la documentation :

  

id   Renvoie «l'identité» d'un objet. Ceci est un entier (ou long   entier) qui est garanti d'être unique et constant pour cet objet   au cours de sa 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 (telles que Jython ou IronPython) pourraient facilement avoir une implémentation différente pour id .

Alors, quel est le cas d'utilisation de est ? PEP8 décrit :

  

Les comparaisons avec des singletons comme None doivent toujours être effectuées avec = ou    n'est pas , jamais les opérateurs d'égalité.

La question

Vous posez et énoncez la question suivante (avec le 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

Ce n'est pas un résultat attendu. Pourquoi est-ce attendu? Cela signifie seulement que les entiers valant 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 de ne pas mémoriser une nouvelle instance distincte 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 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 tente d’être intelligente 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 == .

Qu'est-ce que est

est vérifie que le id de deux

Cela dépend si vous cherchez à savoir si 2 choses sont égales ou le même objet.

est vérifie s'il s'agit du même objet, pas seulement identique. 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

Vous devez utiliser == pour comparer l'égalité des objets arbitraires. Vous pouvez spécifier le comportement avec les attributs __ eq __ et __ ne __ .

Je suis en retard mais vous voulez une source avec votre réponse? *

L’avantage de CPython est que vous pouvez en voir la source. Je vais utiliser des liens pour la version 3.5 pour l'instant; 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 la suivante:

  

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 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 le répertoire principal du code source .

PyLong_FromLong traite des objets longs . Il ne devrait donc pas être difficile de déduire qu'il faut jeter un coup d'œil à l'intérieur de longobject.c . Après avoir regardé à l'intérieur, vous pourriez penser que les choses sont chaotiques; ils sont, mais ne craignez pas, la fonction que nous recherchons est paralysante à ligne 230 en attente de vérification. 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. Vérifions cela:

#define CHECK_SMALL_INT(ival) \
    do if (-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS) { \
        return get_small_int((sdigit)ival); \
    } while(0)

Il s’agit donc d’une macro qui appelle la fonction get_small_int si la valeur ival remplit la condition:

if (-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS)

Que sont donc NSMALLNEGINTS et NSMALLPOSINTS ? Si vous avez deviné les macros, vous n'obtenez rien parce que ce n'était pas une question si difficile. Quoi qu’il en soit, les voici :

#ifndef NSMALLPOSINTS
#define NSMALLPOSINTS           257
#endif
#ifndef NSMALLNEGINTS
#define NSMALLNEGINTS           5
#endif

Notre condition est donc si (-5 < = ival & amp; & amp; ival < 257) appelez get_small_int .

Il n’ya pas d’autre endroit où aller, mais poursuivez votre route en regardant get_small_int dans toute sa splendeur (bon, regardons son corps car c'est ce qui est intéressant):

PyObject *v;
assert(-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS);
v = (PyObject *)&small_ints[ival + NSMALLNEGINTS];
Py_INCREF(v);

OK, 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 il l'est! Nous aurions pu lire la foutue documentation et nous le ferions 'ai toujours su! :

/* 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 voulez 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é.

Depuis la référence

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 vous référez 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 est préférable d'utiliser == à cette fin.

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 soient traités comme des littéraux et que tout ce qui précède soit traité différemment!

Pour les objets de valeur immuables, comme les ints, les chaînes ou les 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’ya pas de différence réelle entre avoir plusieurs références sur le même objet ou plusieurs objets.

est est l'opérateur d'égalité 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 arrive que memoized soit un petit nombre entier, de sorte qu'il a tendance à être fait car ils sont immuables).

l'opérateur === de PHP, est décrit comme vérifiant l'égalité et le type: x == y et le type (x) == le 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

PHP autorise apparemment la même chose pour "intégré". classes (que je veux dire implémenté 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 le 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 valeur. À l'exception des nombres, je veux traiter l'identité de l'objet de la même manière que l'égalité de valeur. "

Cela aurait encore une autre réponse, car nous devons catégoriser les choses en chiffres ou non, pour choisir si nous comparons avec == ou si est . CPython définit le protocole de numérotation , y compris PyNumber_Check, mais ceci n'est pas accessible depuis 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 .Numéro , mais le problème est le même:

import numpy, numbers
assert not issubclass(numpy.int16,numbers.Number)
assert issubclass(int,numbers.Number)

Au fait, NumPy produira des instances distinctes de nombres faibles.

Je ne connais pas vraiment de réponse à cette variante de la question. Je suppose qu’on pourrait théoriquement utiliser ctypes pour appeler PyNumber_Check , mais même cette fonction a été débattu , et ce n'est certainement pas portable. Nous devrons simplement être moins précis sur ce que nous testons pour le moment.

En fin de compte, ce problème provient du fait que Python n'avait pas à l'origine d'arborescence de types avec des prédicats tels que Le code du schéma? ou Haskell's classe de type Num . est vérifie l'identité de l'objet, pas l'égalité de valeur. PHP a aussi une histoire colorée, où === se comporte apparemment comme est uniquement sur les objets en PHP5, mais pas PHP4 . Telles sont les difficultés de plus en plus liées à la mobilité entre les langues (y compris les versions d’une langue).

Il existe un autre problème qui n’est souligné 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 que cela puisse se produire. Une implémentation Python n’est jamais garantie de le faire, mais ils le font tous pour plus que de simples ints.

D'une part, il existe d'autres valeurs pré-créées, telles que le tuple vide, le str et le octets , et certains des chaînes courtes (dans CPython 3.6, il s'agit des 256 chaînes Latin-1 à un caractère). 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 aux int valeurs:

>>> g, h = 42.23e100, 42.23e100
>>> g is h
True

Évidemment, CPython ne vient pas avec une valeur float pré-créée pour 42.23e100 . Alors, que se passe-t-il ici?

Le compilateur CPython fusionnera les valeurs constantes de certains types connus comme int , float , str , octets , dans la même unité de compilation. Pour un module, tout le 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 séparées, 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 n’ya qu’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 finissent pas par être fusionnés:

>>> k, l = (1, 2), (1, 2)
>>> k is l
False

Mettez cela dans une fonction, dis , et regardez le co_consts : il y a un 1 et un 2 , deux (1, 2) tuples qui partagent le même 1 et le 2 mais ne sont pas identiques, et un (( 1, 2), (1, 2)) tuple qui a deux nuplets égaux distincts.

CPython effectue une autre optimisation: l’internalisation de chaînes. Contrairement au pliage constant du compilateur, cela ne se limite pas 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 de stockage interne" compact compact "," compact "ou" compatible ", et dans de nombreux cas, uniquement" compact compact ". sera interné.

Quoi qu’il en soit, les règles concernant les valeurs qui doivent être, peuvent être ou ne peuvent pas être distinctes varient d’une mise en œuvre à l’autre, et entre les versions d’une même implémentation, et peut-être même entre les exécutions du même code sur la même copie de la même mise en œuvre.

Pour le plaisir, il peut être utile d’apprendre les règles d’un Python spécifique. Mais cela ne vaut pas la peine de les utiliser dans votre code. La seule règle de sécurité est:

  • N'écrivez pas de code supposant que deux valeurs immuables égales mais créées séparément soient 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 est pour tester les singletons documentés (comme Aucun ) ou créés uniquement à un endroit du code (comme le _sentinel = idiome de l'objet () ).

Cela se produit également avec les chaînes:

>>> s = b = 'somestr'
>>> s == b, s is b, id(s), id(b)
(True, True, 4555519392, 4555519392)

Maintenant, tout semble aller pour le mieux.

>>> s = 'somestr'
>>> b = 'somestr'
>>> s == b, s is b, id(s), id(b)
(True, True, 4555519392, 4555519392)

C’est ce à quoi on s’attendait 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.

scroll top