Pregunta

¿Por qué se comporta inesperadamente lo siguiente 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

Estoy usando Python 2.5.2. Al probar algunas versiones diferentes de Python, parece que Python 2.3.3 muestra el comportamiento anterior entre 99 y 100.

Basado en lo anterior, puedo suponer que Python se implementa internamente de manera que "pequeño" los enteros se almacenan de manera diferente que los enteros más grandes y el operador is puede notar la diferencia. ¿Por qué la abstracción permeable? ¿Cuál es una mejor manera de comparar dos objetos arbitrarios para ver si son iguales cuando no sé de antemano si son números o no?

¿Fue útil?

Solución

Echa un vistazo a esto:

>>> a = 256
>>> b = 256
>>> id(a)
9987148
>>> id(b)
9987148
>>> a = 257
>>> b = 257
>>> id(a)
11662816
>>> id(b)
11662828

EDITAR: Esto es lo que encontré en la documentación de Python 2, " Plain Integer Objetos " (Es lo mismo para Python 3 ):

  

La implementación actual mantiene un   matriz de objetos enteros para todos   enteros entre -5 y 256, cuando   crear un int en ese rango   en realidad solo volver una referencia a   El objeto existente. Entonces debería ser   posible cambiar el valor de 1. I   sospecha el comportamiento de Python en   Este caso es indefinido. :-)

Otros consejos

  

¿El operador "es" de Python se comporta inesperadamente con enteros?

En resumen, permítanme enfatizar: No use is para comparar enteros.

Este no es un comportamiento sobre el que deberías tener expectativas.

En su lugar, use == y ! = para comparar la igualdad y la desigualdad, respectivamente. Por ejemplo:

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

Explicación

Para saber esto, necesita saber lo siguiente.

Primero, ¿qué hace ? Es un operador de comparación. De la documentación :

  

Los operadores es y no es para verificar la identidad del objeto: x es y es verdadero   si y solo si x e y son el mismo objeto. x no es y produce el   valor de verdad inverso.

Y entonces los siguientes son equivalentes.

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

De la documentación :

  

id   Devuelve la "identidad" de un objeto. Este es un entero (o largo   entero) que se garantiza que es único y constante para este objeto   durante su vida Dos objetos con vidas no superpuestas pueden   tener el mismo valor id () .

Tenga en cuenta que el hecho de que la identificación de un objeto en CPython (la implementación de referencia de Python) es la ubicación en la memoria es un detalle de implementación. Otras implementaciones de Python (como Jython o IronPython) podrían tener fácilmente una implementación diferente para id .

Entonces, ¿cuál es el caso de uso de es ? PEP8 describe :

  

Las comparaciones con singletons como None siempre deben hacerse con is o    no es , nunca los operadores de igualdad.

La pregunta

Usted hace y declara la siguiente pregunta (con código):

  

¿Por qué lo siguiente se comporta inesperadamente en Python?

>>> a = 256
>>> b = 256
>>> a is b
True           # This is an expected result

no es un resultado esperado. ¿Por qué se espera? Solo significa que los enteros valorados en 256 referenciados por a y b son la misma instancia de entero. Los enteros son inmutables en Python, por lo tanto no pueden cambiar. Esto no debería tener ningún impacto en ningún código. No debe esperarse. Es simplemente un detalle de implementación.

Pero quizás deberíamos alegrarnos de que no haya una nueva instancia separada en la memoria cada vez que establezcamos un valor igual a 256.

>>> a = 257
>>> b = 257
>>> a is b
False          # What happened here? Why is this False?

Parece que ahora tenemos dos instancias separadas de enteros con el valor de 257 en la memoria. Como los enteros son inmutables, esto desperdicia memoria. Esperemos que no estemos desperdiciando mucho. Probablemente no lo estemos. Pero este comportamiento no está garantizado.

>>> 257 is 257
True           # Yet the literal numbers compare properly

Bueno, parece que su implementación particular de Python está tratando de ser inteligente y no está creando enteros con valor redundante en la memoria a menos que sea necesario. Parece indicar que está utilizando la implementación de referencia de Python, que es CPython. Bueno para CPython.

Podría ser aún mejor si CPython pudiera hacer esto globalmente, si pudiera hacerlo de manera barata (ya que habría un costo en la búsqueda), tal vez otra implementación podría hacerlo.

Pero en cuanto al impacto en el código, no debería importarle si un entero es una instancia particular de un entero. Solo debe importarle cuál es el valor de esa instancia, y usaría los operadores de comparación normales para eso, es decir, == .

Qué es

is comprueba que el id de dos

Depende de si estás buscando ver si 2 cosas son iguales o el mismo objeto.

is comprueba si son el mismo objeto, no solo iguales. Los pequeños ints probablemente apuntan a la misma ubicación de memoria para la eficiencia del espacio

In [29]: a = 3
In [30]: b = 3
In [31]: id(a)
Out[31]: 500729144
In [32]: id(b)
Out[32]: 500729144

Debe usar == para comparar la igualdad de objetos arbitrarios. Puede especificar el comportamiento con los atributos __eq__ y __ne__ .

Llego tarde pero, ¿quieres alguna fuente con tu respuesta? *

Lo bueno de CPython es que puedes ver la fuente de esto. Voy a utilizar enlaces para la versión 3.5 por ahora; encontrar los correspondientes 2.x es trivial.

En CPython, la función C-API que maneja la creación de un nuevo objeto int es PyLong_FromLong (long v) . La descripción de esta función es:

  

La implementación actual mantiene una matriz de objetos enteros para todos los enteros entre -5 y 256, cuando crea un int en ese rango, en realidad solo obtiene una referencia al objeto existente . Por lo tanto, debería ser posible cambiar el valor de 1. Sospecho que el comportamiento de Python en este caso no está definido. :-)

No sé sobre ti, pero veo esto y pienso: ¡Busquemos esa matriz!

Si no ha jugado con el código C que implementa CPython debería , todo es bastante organizado y legible. Para nuestro caso, debemos buscar en el Objetos / subdirectorio del árbol de directorio del código fuente principal .

PyLong_FromLong trata con objetos largos , por lo que no debería ser difícil deducir que necesitamos mirar dentro de longobject.c . Después de mirar dentro, podrías pensar que las cosas son caóticas; son, pero no temas, la función que estamos buscando es escalofriante en línea 230 esperando que lo revisemos. Es una función pequeña, por lo que el cuerpo principal (sin incluir las declaraciones) se pega fácilmente aquí:

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; 
}

Ahora, no somos C master-code-haxxorz pero tampoco somos tontos, podemos ver que CHECK_SMALL_INT (ival); nos mira a todos seductoramente; Podemos entender que tiene algo que ver con esto. Vamos a verlo:

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

Entonces, es una macro que llama a la función get_small_int si el valor ival cumple la condición:

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

Entonces, ¿qué son NSMALLNEGINTS y NSMALLPOSINTS ? Si adivinó las macros, no obtendrá nada porque esa no era una pregunta tan difícil ... De todos modos, aquí están :

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

Entonces, nuestra condición es if (-5 < = ival & amp; & amp; ival < 257) llame a get_small_int .

No hay otro lugar a donde ir sino continuar nuestro viaje mirando get_small_int en todo su esplendor (bueno, solo veremos su cuerpo porque esas son las cosas interesantes):

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

Bien, declare un PyObject , afirme que la condición anterior se cumple y ejecute la asignación:

v = (PyObject *)&small_ints[ival + NSMALLNEGINTS];

small_ints se parece mucho a la matriz que hemos estado buscando ... ¡y lo es! Podríamos haber leído la maldita documentación y lo haríamos ¡Lo he sabido todo el tiempo! :

/* 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];

Entonces sí, este es nuestro chico. Cuando desee crear un nuevo int en el rango [NSMALLNEGINTS, NSMALLPOSINTS) , simplemente obtendrá una referencia a un objeto ya existente que ha sido previamente asignado.

Desde la referencia

Como puede comprobar en archivo fuente intobject.c , Python almacena en caché enteros pequeños para mayor eficiencia. Cada vez que crea una referencia a un entero pequeño, hace referencia al entero pequeño en caché, no a un objeto nuevo. 257 no es un entero pequeño, por lo que se calcula como un objeto diferente.

Es mejor usar == para ese propósito.

Creo que tus hipótesis son correctas. Experimente con id (identidad del objeto):

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)

¡Parece que los números < = 255 se tratan como literales y todo lo anterior se trata de manera diferente!

Para objetos de valor inmutable, como ints, strings u datetime, la identidad del objeto no es especialmente útil. Es mejor pensar en la igualdad. La identidad es esencialmente un detalle de implementación para los objetos de valor, ya que son inmutables, no hay una diferencia efectiva entre tener múltiples referencias al mismo objeto u objetos múltiples.

es es el operador de igualdad de identidad (funciona como id (a) == id (b) ); es solo que dos números iguales no son necesariamente el mismo objeto. Por razones de rendimiento, algunos enteros pequeños resultan ser memorado por lo que tienden a ser lo mismo (esto puede hacerse ya que son inmutables).

operador de PHP === , por otro lado, se describe como comprobación de igualdad y tipo: x == y y type (x) == type (y) según el comentario de Paulo Freitas. Esto será suficiente para los números comunes, pero difiere de is para las clases que definen __eq__ de manera absurda:

class Unequal:
    def __eq__(self, other):
        return False

PHP aparentemente permite lo mismo para " incorporado " clases (lo que entiendo como implementado a nivel C, no en PHP). Un uso un poco menos absurdo podría ser un objeto temporizador, que tiene un valor diferente cada vez que se usa como un número. Por qué querría emular el Ahora de Visual Basic en lugar de mostrar que es una evaluación con time.time () No lo sé.

Greg Hewgill (OP) hizo un comentario aclaratorio "Mi objetivo es comparar la identidad del objeto, en lugar de la igualdad de valor. Excepto para los números, donde quiero tratar la identidad del objeto igual que la igualdad de valor. & Quot;

Esto tendría otra respuesta, ya que tenemos que clasificar las cosas como números o no, para seleccionar si comparamos con == o is . CPython define el protocolo de número , incluido PyNumber_Check, pero esto no es accesible desde Python mismo.

Podríamos intentar usar isinstance con todos los tipos de números que conocemos, pero esto inevitablemente estaría incompleto. El módulo de tipos contiene una lista de StringTypes pero no NumberTypes. Desde Python 2.6, las clases de números incorporadas tienen una clase base números .Número , pero tiene el mismo problema:

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

Por cierto, NumPy producirá instancias separadas de números bajos.

En realidad no sé una respuesta a esta variante de la pregunta. Supongo que uno podría usar teóricamente ctypes para llamar a PyNumber_Check , pero incluso esa función ha sido debatido , y ciertamente no es portátil. Tendremos que ser menos particulares sobre lo que probamos ahora.

Al final, este problema proviene de que Python originalmente no tenía un árbol de tipos con predicados como El esquema number? o Haskell's tipo de clase Num . is comprueba la identidad del objeto, no la igualdad de valores. PHP también tiene una historia colorida, donde === aparentemente se comporta como es solo en objetos en PHP5, pero no PHP4 . Tales son los crecientes dolores de moverse a través de los idiomas (incluidas las versiones de uno).

Hay otro problema que no se señala en ninguna de las respuestas existentes. Python puede fusionar dos valores inmutables, y los valores int pequeños creados previamente no son la única forma en que esto puede suceder. Una implementación de Python nunca tiene garantizado para hacer esto, pero todos lo hacen por algo más que pequeños ints.


Por un lado, hay algunos otros valores creados previamente, como la tuple , str y bytes vacíos, y algunos cadenas cortas (en CPython 3.6, son las 256 cadenas Latin-1 de un solo carácter). Por ejemplo:

>>> a = ()
>>> b = ()
>>> a is b
True

Pero también, incluso los valores no creados previamente pueden ser idénticos. Considere estos ejemplos:

>>> c = 257
>>> d = 257
>>> c is d
False
>>> e, f = 258, 258
>>> e is f
True

Y esto no se limita a los valores int :

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

Obviamente, CPython no viene con un valor precodificado de float para 42.23e100 . Entonces, ¿qué está pasando aquí?

El compilador CPython fusionará valores constantes de algunos tipos inmutables conocidos como int , float , str , bytes , en la misma unidad de compilación. Para un módulo, todo el módulo es una unidad de compilación, pero en el intérprete interactivo, cada declaración es una unidad de compilación separada. Dado que c y d se definen en declaraciones separadas, sus valores no se fusionan. Como e y f se definen en la misma instrucción, sus valores se fusionan.


Puede ver lo que está sucediendo desmontando el código de bytes. Intente definir una función que haga e, f = 128, 128 y luego llame a dis.dis , y verá que hay un único valor 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

Puede notar que el compilador ha almacenado 128 como una constante, aunque en realidad no lo utiliza el bytecode, lo que le da una idea de la poca optimización que hace el compilador de CPython. Lo que significa que las tuplas (no vacías) en realidad no terminan fusionadas:

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

Ponga eso en una función, dis , y mire los co_consts & # 8212; hay un 1 y un 2 , dos tuplas (1, 2) que comparten las mismas 1 y 2 pero no son idénticas, y un ((1, 2), (1, 2)) tupla que tiene las dos tuplas iguales distintas.


Hay una optimización más que hace CPython: internación de cadenas. A diferencia del compilador de plegado constante, esto no está restringido a literales de código fuente:

>>> m = 'abc'
>>> n = 'abc'
>>> m is n
True

Por otro lado, se limita al tipo str y a cadenas de tipo de almacenamiento interno" ascii compact "," compacto "o" legacy ready ", y en muchos casos solo" ascii compact " será internado.


En cualquier caso, las reglas sobre qué valores deben ser, pueden ser o no distintos pueden variar de una implementación a otra, y entre versiones de la misma implementación, y tal vez incluso entre ejecuciones del mismo código en la misma copia de la misma implementación.

Puede valer la pena aprender las reglas para un Python específico por diversión. Pero no vale la pena confiar en ellos en su código. La única regla segura es:

  • No escriba código que suponga que dos valores inmutables iguales pero creados por separado son idénticos.
  • No escriba código que suponga que dos valores inmutables iguales pero creados por separado son distintos.

O, en otras palabras, solo use is para probar los singletons documentados (como None ) o que solo se crean en un lugar en el código (como el _sentinel = object () idiom).

También sucede con cadenas:

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

Ahora todo parece estar bien.

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

Eso también se espera.

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

Ahora eso es inesperado.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top