python - objetos - El operador ''is'' se comporta inesperadamente con enteros no almacenados en caché
punteros en python (2)
tl; dr:
Como dice el manual de referencia :
Un bloque es una parte del texto del programa Python que se ejecuta como una unidad. Los siguientes son bloques: un módulo, un cuerpo de función y una definición de clase. Cada comando escrito de forma interactiva es un bloque.
Es por eso que, en el caso de una función, tiene un
solo
bloque de código que contiene un
solo
objeto para el literal numérico
1000
, por lo que
id(a) == id(b)
producirá
True
.
En el segundo caso, tiene
dos objetos de código distintos,
cada uno con su propio objeto diferente para el literal
1000
por lo que
id(a) != id(b)
.
Tenga en cuenta que este comportamiento no se manifiesta solo con literales
int
, obtendrá resultados similares con, por ejemplo, literales
float
(consulte
here
).
Por supuesto, la comparación de objetos (excepto las pruebas explícitas
is None
) siempre debe hacerse con el operador de igualdad
==
y
no
is
.
Todo lo indicado aquí se aplica a la implementación más popular de Python, CPython. Otras implementaciones pueden diferir, por lo que no se deben hacer suposiciones al usarlas.
Respuesta larga:
Para obtener una vista un poco más clara y verificar adicionalmente este comportamiento
aparentemente extraño
, podemos mirar directamente en los objetos de
code
para cada uno de estos casos usando el módulo
dis
.
Para la función
func
:
Junto con todos los demás atributos, los objetos de función también tienen un atributo
__code__
que le permite ver el bytecode compilado para esa función.
Usando
dis.code_info
podemos obtener una buena vista de todos los atributos almacenados en un objeto de código para una función dada:
>>> print(dis.code_info(func))
Name: func
Filename: <stdin>
Argument count: 0
Kw-only arguments: 0
Number of locals: 2
Stack size: 2
Flags: OPTIMIZED, NEWLOCALS, NOFREE
Constants:
0: None
1: 1000
Variable names:
0: a
1: b
Solo nos interesa la entrada
Constants
para la función
func
.
En él, podemos ver que tenemos dos valores,
None
(siempre presente) y
1000
.
Solo tenemos una
única
instancia int que representa la constante
1000
.
Este es el valor al que se asignarán a y
b
cuando se invoque la función.
Acceder a este valor es fácil a través de
func.__code__.co_consts[1]
y así, otra forma de ver nuestra evaluación a
a is b
en la función sería así:
>>> id(func.__code__.co_consts[1]) == id(func.__code__.co_consts[1])
Lo cual, por supuesto, se evaluará como
True
porque nos estamos refiriendo al mismo objeto.
Para cada comando interactivo:
Como se señaló anteriormente, cada comando interactivo se interpreta como un solo bloque de código: analizado, compilado y evaluado de forma independiente.
Podemos obtener los objetos de código para cada comando a través de la
compile
incorporada:
>>> com1 = compile("a=1000", filename="", mode="single")
>>> com2 = compile("b=1000", filename="", mode="single")
Para cada declaración de asignación, obtendremos un objeto de código de aspecto similar al siguiente:
>>> print(dis.code_info(com1))
Name: <module>
Filename:
Argument count: 0
Kw-only arguments: 0
Number of locals: 0
Stack size: 1
Flags: NOFREE
Constants:
0: 1000
1: None
Names:
0: a
El mismo comando para
com2
ve igual pero
tiene una diferencia fundamental
: cada uno de los objetos de código
com1
y
com2
tienen instancias int diferentes que representan el literal
1000
.
Es por eso que, en este caso, cuando hacemos
a is b
través del argumento
co_consts
, en realidad obtenemos:
>>> id(com1.co_consts[0]) == id(com2.co_consts[0])
False
Lo que está de acuerdo con lo que realmente obtuvimos.
Diferentes objetos de código, diferentes contenidos.
Nota: Tenía algo de curiosidad sobre cómo sucede exactamente esto en el código fuente y, después de analizarlo, creo que finalmente lo encontré.
Durante la fase de compilaciones, el atributo
co_consts
está representado por un objeto de diccionario.
En
compile.c
podemos ver la inicialización:
/* snippet for brevity */
u->u_lineno = 0;
u->u_col_offset = 0;
u->u_lineno_set = 0;
u->u_consts = PyDict_New();
/* snippet for brevity */
Durante la compilación, esto se verifica para constantes ya existentes. Vea la respuesta de @Raymond Hettinger a continuación para obtener un poco más sobre esto.
Advertencias:
-
Las declaraciones encadenadas evaluarán una verificación de identidad de
True
Ahora debería quedar más claro por qué exactamente lo siguiente se evalúa como
True
:>>> a = 1000; b = 1000; >>> a is b
En este caso, al encadenar los dos comandos de asignación juntos le decimos al intérprete que los compile juntos . Como en el caso del objeto de función, solo se creará un objeto para el literal
1000
dará como resultado un valorTrue
cuando se evalúe. -
La ejecución a nivel de módulo produce
True
nuevamente:Como se mencionó anteriormente, el manual de referencia establece que:
... Los siguientes son bloques: un módulo ...
Por lo tanto, se aplica la misma premisa: tendremos un único objeto de código (para el módulo) y, como resultado, valores únicos almacenados para cada literal diferente.
-
Lo mismo no se aplica a los objetos mutables :
Lo que significa que a menos que inicialicemos explícitamente al mismo objeto mutable (por ejemplo con a = b = []), la identidad de los objetos nunca será igual, por ejemplo:
a = []; b = [] a is b # always returns false
Nuevamente, en la documentación , esto se especifica:
después de a = 1; b = 1, ayb pueden o no referirse al mismo objeto con el valor uno, dependiendo de la implementación, pero después de c = []; d = [], cyd están garantizados para referirse a dos listas vacías diferentes, únicas y recién creadas.
Al jugar con el intérprete de Python, me topé con este caso conflictivo con respecto al operador
is
:
Si la evaluación tiene lugar en la función, devuelve
True
, si se realiza fuera, devuelve
False
.
>>> def func():
... a = 1000
... b = 1000
... return a is b
...
>>> a = 1000
>>> b = 1000
>>> a is b, func()
(False, True)
Dado que el operador
is
evalúa los
id()
para los objetos involucrados, esto significa que
a
y
b
apuntan a la misma instancia
int
cuando se declara dentro de la función
func
pero, por el contrario, apuntan a un objeto diferente cuando están fuera de ella. .
¿Por qué esto es tan?
Nota
: Soy consciente de la diferencia entre las operaciones de identidad (
is
) e igualdad (
==
) como se describe en
Comprender el operador "is" de Python
.
Además, también soy consciente del almacenamiento en caché que Python está realizando para los enteros en el rango
[-5, 256]
como se describe en el
operador "es" se comporta inesperadamente con los enteros
.
Este
no es el caso aquí
ya que los números están fuera de ese rango y quiero evaluar la identidad y
no la
igualdad.
En la solicitud interactiva, las entradas se compilan en un solo modo que procesa una declaración completa a la vez. El compilador en sí (en Python/compile.c ) rastrea las constantes en un diccionario llamado u_consts que asigna el objeto constante a su índice.
En la función compiler_add_o() , ve que antes de agregar una nueva constante (e incrementar el índice), se verifica el dict para ver si el objeto constante y el índice ya existen. Si es así, se reutilizan.
En resumen, eso significa que las constantes repetidas en una declaración (como en la definición de su función) se pliegan en un solo tono.
En contraste, su
a = 1000
b = 1000
son dos declaraciones separadas, por lo que no se produce el plegamiento.
FWIW, todo esto es solo un detalle de implementación de CPython (es decir, no está garantizado por el lenguaje). Esta es la razón por la cual las referencias aquí dadas son al código fuente C en lugar de la especificación del lenguaje, que no garantiza el tema.
Espero que hayas disfrutado de esta idea de cómo funciona CPython bajo el capó :-)