strings operator logical example assignment python int comparison operators identity

python - operator - El operador “is” se comporta inesperadamente con enteros



python logical operators example (11)

El operador “is” de Python se comporta inesperadamente con los enteros?

En resumen, permítanme enfatizar: no usar is comparar números enteros.

Esto no es un comportamiento sobre el que deberías tener alguna expectativa.

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, necesitas saber lo siguiente.

Primero, que hace is hacer? Es un operador de comparación. De la documentation :

Los operadores is y is not prueban la identidad del objeto: x is y es verdadero si y solo si x e y son el mismo objeto. x is not y produce el valor de verdad inverso.

Y así, los siguientes son equivalentes.

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

De la documentation :

id Devuelve la "identidad" de un objeto. Este es un entero (o entero largo) que se garantiza que será único y constante para este objeto durante su vida útil. Dos objetos con tiempos de vida no superpuestos 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) sea la ubicación en la memoria es un detalle de la implementación. Otras implementaciones de Python (como Jython o IronPython) podrían fácilmente tener una implementación diferente para id .

Entonces, ¿para qué sirve el caso? PEP8 describe :

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

La pregunta

Usted pregunta e indica 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 valuados 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 indiquemos 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 de valor redundante en la memoria a menos que tenga que hacerlo. Parece indicar que está utilizando la implementación de referencia de Python, que es CPython. Bueno para CPython.

Podría ser incluso mejor si CPython pudiera hacer esto a nivel mundial, si pudiera hacerlo a un precio bajo (como 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 debe preocuparse si un entero es una instancia particular de un entero. Solo debe importar cuál es el valor de esa instancia, y usaría los operadores de comparación normales para eso, es decir, == .

Lo is hace

is comprueba que el id de dos objetos sea el mismo. En CPython, el 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 reafirmar esto con el código:

>>> a is b

es lo mismo que

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

¿Por qué querríamos usar is entonces?

Esto puede ser una verificación muy rápida en relación a decir, verificando si dos cadenas muy largas tienen el mismo valor. Pero como se aplica a la singularidad 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 instancia única que existe en un lugar en la memoria). Podríamos crear otros singletons si existe la posibilidad de combinarlos, lo que podríamos verificar is , pero estos son relativamente raros. Aquí hay un ejemplo (funcionará en Python 2 y 3) por ejemplo

SENTINEL_SINGLETON = object() # this will only be created one time. def foo(keyword_argument=None): if keyword_argument is None: print(''no argument given to foo'') bar() bar(keyword_argument) bar(''baz'') def bar(keyword_argument=SENTINEL_SINGLETON): # SENTINEL_SINGLETON tells us if we were not passed anything # as None is a legitimate potential argument we could get. if keyword_argument is SENTINEL_SINGLETON: print(''no argument given to bar'') else: print(''argument to bar: {0}''.format(keyword_argument)) foo()

Que imprime:

no argument given to foo no argument given to bar argument to bar: None argument to bar: baz

Y así vemos, con is y un centinela, podemos diferenciar cuando se llama a la bar sin argumentos y cuando se llama con None . Estos son los principales casos de uso para is - no lo use para probar la igualdad de enteros, cadenas, tuplas u otras cosas como estas.

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

En base a lo anterior, puedo suponer que Python se implementa internamente de tal manera que los enteros "pequeños" se almacenan de una manera diferente a los enteros más grandes y el operador is capaz de distinguir 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?


Como puede ingresar en el archivo fuente en objeto.c , Python almacena en caché enteros pequeños para mejorar la eficiencia. Cada vez que crea una referencia a un entero pequeño, está refiriendo el entero pequeño en caché, no un nuevo objeto. 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. Experimentar 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!


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

is cheques para ver si son el mismo objeto, no solo iguales. Las pequeñas entradas probablemente apuntan a la misma ubicación de memoria para la eficiencia de 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 __ne__ y __ne__ .


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 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ñas intenciones.

Por un lado, hay algunos otros valores creados previamente, como la tuple vacía, la str y los bytes , y algunas 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 de int :

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

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

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

Puede ver lo que está sucediendo al desensamblar el código de bytes. Intente definir una función que haga e, f = 128, 128 y luego llame a dis.dis en ella, y verá que hay un solo 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 observar que el compilador ha almacenado 128 como una constante a pesar de que el bytecode no lo utiliza, 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 se fusionan:

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

Póngalo en una función, co_consts y observe los co_consts : hay un 1 y un 2 , dos (1, 2) tuplas que comparten el mismo 1 y 2 pero no son idénticos, y un ((1, 2), (1, 2)) tupla que tiene dos tuplas iguales distintas.

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

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

Por otro lado, está limitado al tipo str , y a las cadenas de almacenamiento interno tipo "ascii compact", "compact" o "legacy ready" , y en muchos casos solo se internará "ascii compact".

En cualquier caso, las reglas de qué valores deben ser, pueden o no pueden ser distintas varían de una implementación a otra, y entre las 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 de un Python específico por diversión. Pero no vale la pena confiar en ellos en tu código. La única regla segura es:

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

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


Mira here

La implementación actual mantiene una matriz de objetos enteros para todos los enteros entre -5 y 256, cuando creas un int en ese rango, en realidad simplemente recuperas una referencia al objeto existente.


Mira 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, "Objetos de entero llano" (es lo mismo para Python 3 ):

La implementación actual mantiene una matriz de objetos enteros para todos los enteros entre -5 y 256, cuando creas un int en ese rango, en realidad simplemente recuperas 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. :-)


Para los objetos de valor inmutable, como ints, cadenas o tiempos de datos, 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 varias referencias al mismo objeto o varios objetos.


También sucede con las cuerdas:

>>> 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 es lo esperado también.

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


is el operador de igualdad de identidad (que 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 se memoized por lo que tienden a ser los mismos (esto se puede hacer ya que son inmutables).

PHP''s otro lado, el operador de PHP''s === se describe como verificació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 las clases que definen __eq__ de una manera absurda:

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

PHP aparentemente permite lo mismo para las clases "incorporadas" (lo que entiendo que significa 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 número. No sé por qué querría emular el Visual Basic Now lugar de mostrar que es una evaluación con time.time() .

Greg Hewgill (OP) hizo un comentario clarificador "Mi objetivo es comparar la identidad del objeto, en lugar de la igualdad de valores. Excepto por los números, donde quiero tratar la identidad de objetos de la misma manera que la igualdad de valores".

Esto tendría aún 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 del número , incluido PyNumber_Check, pero no se puede acceder a este desde Python.

Podríamos tratar de usar esta isinstance con todos los tipos de números que conocemos, pero esto inevitablemente sería incompleto. El módulo de tipos contiene una lista de StringTypes pero ningún NumberTypes. Desde Python 2.6, las clases de números incorporadas tienen un número de clase base numbers.Number 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 ctypes teóricamente para llamar a PyNumber_Check , pero incluso esa función ha sido debatida , y ciertamente no es portátil. Tendremos que ser menos precisos acerca de lo que probamos por ahora.

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


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

Lo bueno de CPython es que realmente puedes ver la fuente de esto. Voy a usar 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 para esta función es:

La implementación actual mantiene una matriz de objetos enteros para todos los enteros entre -5 y 256, cuando creas un int en ese rango, en realidad simplemente recuperas 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: ¡Encontremos esa matriz!

Si no has jugado con el código C que implementa CPython, deberías hacerlo , todo está bastante organizado y es fácil de leer. Para nuestro caso, debemos buscar en el subdirectorio Objects/ del árbol de directorios del código fuente principal .

PyLong_FromLong trata con objetos long , 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; lo son, pero no teman, la función que estamos buscando es relajarnos en la line 230 esperando que lo comprobemos. Es una función pequeña, por lo que el cuerpo principal (excluyendo las declaraciones) se puede pegar 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); mirándonos a todos seductoramente; Podemos entender que tiene algo que ver con esto. Vamos a ver:

#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 adivinaste que las macros no obtienes nada porque no fue 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 && ival < 257) llame a get_small_int .

No hay otro lugar al que ir, pero continúe nuestro viaje observando get_small_int en todo su esplendor (bueno, solo miraremos su cuerpo porque esas son las cosas interesantes):

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

De acuerdo, declare un PyObject , afirme que la condición anterior es válida y ejecute la asignación:

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

small_ints parece mucho a la matriz que hemos estado buscando ... ¡y lo es! ¡Podríamos haber leído la maldita documentación y lo sabríamos 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];

Así que 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 preasignado.

Dado que la referencia se refiere al mismo objeto, la emisión de id() directamente o la verificación de la identidad is activada, devolverá exactamente lo mismo.

Pero, ¿cuándo se asignan?

Durante la inicialización en _PyLong_Init Python entrará gustosamente en un bucle for, haga esto por usted:

for (ival = -NSMALLNEGINTS; ival < NSMALLPOSINTS; ival++, v++) { // Look me up! }

Espero que mi explicación te haya hecho C (juego de palabras obviamente intencional) las cosas claramente ahora.

Pero, ¿257 es 257? ¿Que pasa?

Esto es realmente más fácil de explicar, y ya he intentado hacerlo ; se debe a que Python ejecutará esta declaración interactiva:

>>> 257 is 257

como un solo bloque. Durante la compilación de esta declaración, CPython verá que tiene dos literales coincidentes y usará el mismo PyLongObject representa 257 . Puedes ver esto si haces la compilación y examinas su contenido:

>>> codeObj = compile("257 is 257", "blah!", "exec") >>> codeObj.co_consts (257, None)

Cuando CPython hace la operación; ahora solo va a cargar el mismo objeto exacto:

>>> import dis >>> dis.dis(codeObj) 1 0 LOAD_CONST 0 (257) # dis 3 LOAD_CONST 0 (257) # dis again 6 COMPARE_OP 8 (is)

Así será el regreso True .

* - Intentaré escribir esto de una manera más introductoria para que la mayoría pueda seguirlo.