¿Por qué lo siguiente se comporta inesperadamente 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 plantear la hipótesis de que Python se implementa internamente de modo que los enteros "pequeños" se almacenan de manera diferente a los enteros más grandes y el is
operador 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?
Respuestas:
Mira esto:
Esto es lo que encontré en la documentación de Python 2, "Objetos enteros simples" (es lo mismo para Python 3 ):
fuente
En resumen, permítanme enfatizar: no lo 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:Explicación
Para saber esto, necesita saber lo siguiente.
Primero, ¿qué hace
is
? Es un operador de comparación. De la documentación :Y así, los siguientes son equivalentes.
De la documentación :
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, ¿para qué es el caso de uso
is
? PEP8 describe :La pregunta
Usted hace y declara la siguiente pregunta (con código):
Es no un resultado esperado. ¿Por qué se espera? Solo significa que los enteros valorados en
256
referenciados por ambosa
yb
son la misma instancia de entero. Los enteros son inmutables en Python, por lo tanto no pueden cambiar. Esto no debería tener 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.
Parece que ahora tenemos dos instancias separadas de enteros con el valor de
257
en 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.Bueno, esto 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 a bajo costo (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
==
.Que
is
haceis
comprueba que losid
dos objetos son iguales. En CPython,id
es la ubicación en la memoria, pero podría ser algún otro número de identificación único en otra implementación. Para reformular esto con código:es lo mismo que
¿Por qué querríamos usar
is
entonces?Esto puede ser una verificación muy rápida en relación con decir, verificando si dos cadenas muy largas tienen el mismo valor. Pero como se aplica a la unicidad del objeto, tenemos casos de uso limitados para él. De hecho, en su mayoría queremos usarlo para verificar
None
, que es un singleton (una única instancia que existe en un lugar en la memoria). Podríamos crear otros singletons si existe el potencial de combinarlos, con lo que podríamos verificaris
, pero estos son relativamente raros. Aquí hay un ejemplo (funcionará en Python 2 y 3) ej.Que imprime:
Y así vemos, con
is
y un centinela, somos capaces de diferenciar cuándobar
se llama sin argumentos y cuándo se llama conNone
. Estos son los principales casos de uso parais
: no lo use para probar la igualdad de enteros, cadenas, tuplas u otras cosas como estas.fuente
is
- no lo use para probar la igualdad de enteros, cadenas, tuplas u otras cosas como estas". Sin embargo, estoy tratando de integrar una máquina de estado simple en mi clase, y dado que los estados son valores opacos cuya única propiedad observable es la de ser idénticos o diferentes, parece bastante natural que sean comparables con ellosis
. Planeo usar cadenas internas como estados. Hubiera preferido enteros simples, pero desafortunadamente Python no puede integrar enteros (0 is 0
es un detalle de implementación).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. Las entradas pequeñas probablemente apuntan a la misma ubicación de memoria para ahorrar espacioDebe usar
==
para comparar la igualdad de objetos arbitrarios. Puede especificar el comportamiento con los atributos__eq__
y__ne__
.fuente
Llego tarde pero, ¿quieres alguna fuente con tu respuesta? Intentaré redactar esto de manera introductoria para que más personas puedan seguirlo.
Lo bueno de CPython es que realmente puedes ver la fuente de esto. Voy a usar enlaces para la versión 3.5 , pero encontrar los correspondientes 2.x es trivial.
En CPython, la función C-API que maneja la creación de un nuevo
int
objeto esPyLong_FromLong(long v)
. La descripción de esta función es:(Mi cursiva)
No sé sobre ti, pero veo esto y pienso: ¡encontremos esa matriz!
Si no ha jugado con el código C que implementa CPython , debería hacerlo ; todo es bastante organizado y legible. Para nuestro caso, necesitamos mirar en el
Objects
subdirectorio del árbol de directorios del código fuente principal .PyLong_FromLong
trata conlong
objetos, por lo que no debería ser difícil deducir que necesitamos echar un vistazo dentrolongobject.c
. Después de mirar dentro, puede pensar que las cosas son caóticas; son, pero no temas, la función que estamos buscando es escalofriante línea 230 esperando que la revisemos. Es una función pequeña, por lo que el cuerpo principal (excluyendo las declaraciones) se pega fácilmente aquí: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 ver:Entonces, es una macro que llama a la función
get_small_int
si el valorival
cumple la condición:Entonces, ¿qué son
NSMALLNEGINTS
yNSMALLPOSINTS
? Macros! Aquí están :Entonces nuestra condición es
if (-5 <= ival && ival < 257)
llamadaget_small_int
.A continuación, veamos
get_small_int
en todo su esplendor (bueno, solo veremos su cuerpo porque ahí es donde están las cosas interesantes):De acuerdo, declarar un
PyObject
, afirme que la condición anterior se cumple y ejecute la asignación:small_ints
se parece mucho a esa matriz que hemos estado buscando, ¡y lo es! ¡Podríamos haber leído la maldita documentación y lo habríamos sabido todo el tiempo!:Así que sí, este es nuestro chico. Cuando quieras crear un nuevo
int
en el rango[NSMALLNEGINTS, NSMALLPOSINTS)
, simplemente obtendrá una referencia a un objeto ya existente que ha sido previamente asignado.Como la referencia se refiere al mismo objeto, la emisión
id()
directamente o verificar la identidad conis
él devolverá exactamente lo mismo.Pero, ¿cuándo se asignan?
Durante la inicialización en
_PyLong_Init
Python con mucho gusto entrará en un bucle for, haga esto por usted:¡Mira la fuente para leer el cuerpo del bucle!
Espero que mi explicación te haya aclarado las cosas ahora (juego de palabras obviamente intencionado).
Pero,
257 is 257
? ¿Qué pasa?En realidad, esto es más fácil de explicar, y ya he intentado hacerlo ; se debe al hecho de que Python ejecutará esta declaración interactiva como un solo bloque:
Durante la compilación de esta declaración, CPython verá que tiene dos literales coincidentes y usará la misma
PyLongObject
representación257
. Puede ver esto si hace la compilación usted mismo y examina su contenido:Cuando CPython realiza la operación, ahora solo va a cargar exactamente el mismo objeto:
Entonces
is
volveremosTrue
.fuente
Como puedes comprobar en el 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.fuente
Creo que tus hipótesis son correctas. Experimente con
id
(identidad del objeto):¡Parece que los números
<= 255
se tratan como literales y todo lo anterior se trata de manera diferente!fuente
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.
fuente
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. Nunca se garantiza que una implementación de Python haga esto, pero todos lo hacen por algo más que pequeños ints.
Por un lado, hay algunos otros valores creados previamente, como el vacío
tuple
,str
ybytes
, y algunas cadenas cortas (en CPython 3.6, que es la única de 256 caracteres cadenas Latin-1). Por ejemplo:Pero también, incluso los valores no creados previamente pueden ser idénticos. Considere estos ejemplos:
Y esto no se limita a los
int
valores:Obviamente, CPython no viene con un
float
valor pre-creado para42.23e100
. Entonces, ¿qué está pasando aquí?El compilador CPython se fusionará valores constantes de algunos tipos conocida como inmutables
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. Comoc
yd
se definen en declaraciones separadas, sus valores no se fusionan. Ya quee
yf
se definen en la misma declaración, sus valores se fusionan.Puede ver lo que está sucediendo desmontando el código de bytes. Intente definir una función que lo haga
e, f = 128, 128
y luego invocarladis.dis
, y verá que hay un único valor constante(128, 128)
Puede notar que el compilador se ha almacenado
128
como una constante a pesar de que el bytecode no lo utiliza realmente, 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:Póngalo en una función,
dis
y mireco_consts
: hay a1
y a2
, dos(1, 2)
tuplas que comparten lo mismo1
y que2
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 plegado constante del compilador, esto no está restringido a literales de código fuente:
Por otro lado, se limita al
str
tipo y a cadenas de tipo de almacenamiento interno "ascii compact", "compact" o "legacy ready" , y en muchos casos solo se internará "ascii compact".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:
x is y
, usex == y
)x is not y
, usex != y
)O, en otras palabras, solo use
is
para probar los singletons documentados (comoNone
) o que solo se crean en un lugar en el código (como el_sentinel = object()
idioma).fuente
x is y
para comparar, usarx == y
. Del mismo modo, no usex is not y
, usex != y
a=257; b=257
en una sola línea?a is b
Verdaderois
es el operador de igualdad de identidad (funciona comoid(a) == id(b)
); es solo que dos números iguales no son necesariamente el mismo objeto. Por razones de rendimiento, algunos números enteros pequeños se recuerdan por lo que tienden a ser los mismos (esto se puede hacer ya que son inmutables).El
===
operador de PHP , por otro lado, se describe como comprobación de igualdad y tipo:x == y and type(x) == type(y)
según el comentario de Paulo Freitas. Esto será suficiente para los números comunes, pero difiere de lasis
clases que definen__eq__
de manera absurda:Aparentemente, PHP permite lo mismo para las clases "integradas" (que entiendo que se implementan 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 Visual Basic
Now
lugar de mostrar que es una evaluación contime.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".
Esto tendría otra respuesta, ya que tenemos que clasificar las cosas como números o no, para seleccionar si nos comparamos con
==
ois
. CPython define el protocolo de número , incluido PyNumber_Check, pero esto no es accesible desde Python.Podríamos intentar usarlo
isinstance
con todos los tipos de números que conocemos, pero esto inevitablemente estaría incompleto. El módulo de tipos contiene una lista StringTypes pero no NumberTypes. Desde Python 2.6, las clases de números incorporadas tienen una clase basenumbers.Number
, pero tiene el mismo problema: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
PyNumber_Check
, pero incluso esa función ha sido debatida , y ciertamente no es portátil. Tendremos que ser menos particulares sobre lo que probamos por ahora.Al final, este problema se debe a que Python originalmente no tenía un árbol de tipos con predicados como Scheme
number?
o la clase de tipo Num de Haskell .is
comprueba la identidad del objeto, no la igualdad de valores. PHP también tiene una historia colorida, donde===
aparentemente se comportais
solo en objetos en PHP5, pero no en PHP4 . Tales son los crecientes dolores de moverse a través de los idiomas (incluidas las versiones de uno).fuente
También sucede con cadenas:
Ahora todo parece estar bien.
Eso también se espera.
Ahora eso es inesperado.
fuente
'xx'
es como se esperaba, como es'xxx'
, pero'x x'
no lo es.xx
en alguna parte de su sesión de Python, esa cadena ya está internada; y puede haber una heurística que lo haga si solo se parece a un nombre. Al igual que con los números, esto se puede hacer porque son inmutables. docs.python.org/2/library/functions.html#intern guilload.com/python-string-interningNovedades de Python 3.8: Cambios en el comportamiento de Python :
fuente