En Python, cuando dos objetos son iguales?
title in python plot (2)
Python tiene algunos tipos que garantiza que solo tendrán una instancia. Ejemplos de estas instancias son None
, NotImplemented
y Ellipsis
. Estos son (por definición) singletons y, por lo tanto, se garantiza que los elementos como None is None
devuelven True
porque no hay forma de crear una nueva instancia de NoneType
.
También proporciona unos pocos doubletons 1 True
, False
2 - Todas las referencias al punto True
para el mismo objeto. De nuevo, esto se debe a que no hay forma de crear una nueva instancia de bool
.
Todo lo anterior está garantizado por el lenguaje Python. Sin embargo, como habrás notado, hay algunos tipos (todos inmutables) que almacenan algunas instancias para su reutilización. Esto está permitido por el lenguaje, pero diferentes implementaciones pueden optar por usar esta concesión o no, según sus estrategias de optimización. Algunos ejemplos que se incluyen en esta categoría son enteros pequeños (-5 -> 255), la tuple
vacía y el conjunto de frozenset
vacío.
Finalmente, Cpython internó ciertos objetos inmutables durante el análisis ...
por ejemplo, si ejecuta el siguiente script con Cpython, verá que devuelve True
:
def foo():
return (2,)
if __name__ == ''__main__'':
print foo() is foo()
Esto parece realmente extraño. El truco que está jugando Cpython es que cada vez que construye la función foo
, ve una tupla-literal que contiene otros literales simples (inmutables). En lugar de crear esta tupla (o sus equivalentes) una y otra vez, Python solo la crea una vez. No hay peligro de que se cambie ese objeto, ya que todo el trato es inmutable. Esto puede ser una gran ganancia para el rendimiento donde se llama el mismo ciclo cerrado una y otra vez. También se internan pequeñas cuerdas. La verdadera victoria aquí es en búsquedas de diccionarios. Python puede hacer una comparación de punteros (sorprendentemente rápida) y luego recurrir a comparaciones de cadenas más lentas cuando comprueba las colisiones hash. Dado que gran parte de python se basa en las búsquedas de diccionario, esto puede ser una gran optimización para el lenguaje en general.
1 Podría haber inventado esa palabra ... Pero espero que entiendas la idea ...
2 En circunstancias normales, no es necesario comprobar si el objeto es una referencia a True
: por lo general, solo le importa si el objeto es "sincero", por ejemplo, if some_instance: ...
ejecutará la bifurcación. Pero, puse eso aquí para completarlo.
Tenga en cuenta que is
puede usar para comparar cosas que no son singletons. Un uso común es crear un valor centinela:
sentinel = object()
item = next(iterable, sentinel)
if items is sentinel:
# iterable exhausted.
O:
_sentinel = object()
def function(a, b, none_is_ok_value_here=_sentinel):
if none_is_ok_value_here is sentinel:
# Treat the function as if `none_is_ok_value_here` was not provided.
La moraleja de esta historia es decir siempre lo que quieres decir. Si desea verificar si un valor es otro valor, utilice el operador is
. Si desea verificar si un valor es igual a otro valor (pero posiblemente distinto), entonces use ==
. Para obtener más detalles sobre la diferencia entre is
y ==
(y cuándo usar cuál), consulte una de las siguientes publicaciones:
- ¿Hay alguna diferencia entre `==` y `is` en Python?
- Python Ninguna comparación: ¿debería usar "es" o ==?
Apéndice
Hemos hablado sobre estos detalles de implementación de CPython y hemos afirmado que son optimizaciones. Sería bueno tratar de medir exactamente lo que obtenemos de toda esta optimización (aparte de un poco de confusión añadida al trabajar con el operador is
).
String "interning" y búsquedas en diccionarios.
Aquí hay una pequeña secuencia de comandos que puede ejecutar para ver qué tan rápido son las búsquedas en el diccionario si usa la misma cadena para buscar el valor en lugar de una cadena diferente. Tenga en cuenta que utilizo el término "internado" en los nombres de las variables: estos valores no están necesariamente internados (aunque podrían serlo). Solo estoy usando eso para indicar que la cadena "interna" es la cadena del diccionario.
import timeit
interned = ''foo''
not_interned = (interned + '' '').strip()
assert interned is not not_interned
d = {interned: ''bar''}
print(''Timings for short strings'')
number = 100000000
print(timeit.timeit(
''d[interned]'',
setup=''from __main__ import interned, d'',
number=number))
print(timeit.timeit(
''d[not_interned]'',
setup=''from __main__ import not_interned, d'',
number=number))
####################################################
interned_long = interned * 100
not_interned_long = (interned_long + '' '').strip()
d[interned_long] = ''baz''
assert interned_long is not not_interned_long
print(''Timings for long strings'')
print(timeit.timeit(
''d[interned_long]'',
setup=''from __main__ import interned_long, d'',
number=number))
print(timeit.timeit(
''d[not_interned_long]'',
setup=''from __main__ import not_interned_long, d'',
number=number))
Los valores exactos aquí no deberían importar demasiado, pero en mi computadora, las cuerdas cortas muestran aproximadamente 1 parte en 7 más rápido. Las cadenas largas son casi 2 veces más rápidas (porque la comparación de cadenas tarda más si la cadena tiene más caracteres para comparar). Las diferencias no son tan llamativas en python3.x, pero todavía están definitivamente allí.
Tuple "interno"
Aquí hay un pequeño script con el que puedes jugar:
import timeit
def foo_tuple():
return (2, 3, 4)
def foo_list():
return [2, 3, 4]
assert foo_tuple() is foo_tuple()
number = 10000000
t_interned_tuple = timeit.timeit(''foo_tuple()'', setup=''from __main__ import foo_tuple'', number=number)
t_list = (timeit.timeit(''foo_list()'', setup=''from __main__ import foo_list'', number=number))
print(t_interned_tuple)
print(t_list)
print(t_interned_tuple / t_list)
print(''*'' * 80)
def tuple_creation(x):
return (x,)
def list_creation(x):
return [x]
t_create_tuple = timeit.timeit(''tuple_creation(2)'', setup=''from __main__ import tuple_creation'', number=number)
t_create_list = timeit.timeit(''list_creation(2)'', setup=''from __main__ import list_creation'', number=number)
print(t_create_tuple)
print(t_create_list)
print(t_create_tuple / t_create_list)
Este es un poco más complicado de configurar (y me complace tomar algunas ideas mejores sobre cómo cronometrarlo en los comentarios). La esencia de esto es que, en promedio (y en mi computadora), una tupla tarda aproximadamente 60% en crearse como lo hace una lista. Sin embargo, foo_tuple()
toma en promedio aproximadamente 40% del tiempo que toma foo_list()
. Eso muestra que realmente ganamos un poco más de velocidad de estos pasantes. El ahorro de tiempo parece aumentar a medida que la tupla se hace más grande (crear una lista más larga lleva más tiempo: la "creación" de la tupla lleva un tiempo constante ya que ya se creó).
También tenga en cuenta que he llamado a esto "internamiento". En realidad no lo es (al menos no en el mismo sentido en que las cuerdas están intercaladas). Podemos ver la diferencia en este simple script:
def foo_tuple():
return (2,)
def bar_tuple():
return (2,)
def foo_string():
return ''foo''
def bar_string():
return ''foo''
print(foo_tuple() is foo_tuple()) # True
print(foo_tuple() is bar_tuple()) # False
print(foo_string() is bar_string()) # True
Vemos que las cadenas están realmente "internados": diferentes invocaciones que usan la misma notación literal devuelven el mismo objeto. La tupla "interning" parece ser específica de una sola línea.
Parece que 2 is 2
y 3 is 3
siempre será verdadero en python, y en general, cualquier referencia a un entero es igual a cualquier otra referencia al mismo número entero. Lo mismo le sucede a None
(es decir, None is None
). Sé que esto no sucede con los tipos definidos por el usuario o los tipos mutables. Pero a veces también falla en tipos inmutables:
>>> () is ()
True
>>> (2,) is (2,)
False
Es decir: dos construcciones independientes de las tuplas de rendimiento vacías hacen referencia al mismo objeto en la memoria, pero dos construcciones independientes de tuplas de elementos idénticos uno (inmutable) terminan creando dos objetos idénticos. Probé, y el trabajo de frozenset
de una manera similar a las tuplas.
¿Qué determina si un objeto se duplicará en la memoria o tendrá una sola instancia con muchas referencias? ¿Depende de si el objeto es "atómico" en algún sentido? ¿Varía según la implementación?
Varía según la implementación.
CPython almacena en caché algunos objetos inmutables en la memoria. Esto es cierto para enteros "pequeños" como 1 y 2 (-5 a 255, como se observa en los comentarios a continuación). CPython lo hace por razones de rendimiento; Los enteros pequeños se usan comúnmente en la mayoría de los programas, por lo que se ahorra memoria para que solo se cree una copia (y es seguro porque los enteros son inmutables).
Esto también es cierto para objetos "singleton" como None
; solo existe una None
en existencia en un momento dado.
Otros objetos (como la tupla vacía, ()
) pueden implementarse como singletons, o pueden no serlo.
En general, no debe asumir necesariamente que los objetos inmutables se implementarán de esta manera. CPython lo hace por razones de rendimiento, pero otras implementaciones pueden no hacerlo, y CPython puede incluso dejar de hacerlo en algún momento en el futuro. (La única excepción podría ser None
, ya que x is None
es un idioma común de Python y es probable que se implemente en diferentes intérpretes y versiones).
Por lo general, desea utilizar ==
lugar de is
. Python''s is
operator no se usa con frecuencia, excepto cuando se comprueba si una variable es None
.