punteros objetos objeto memoria manejo maneja eliminar dinámica destruir como asignación python python-3.x int identity python-internals

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 valor True 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ó :-)