Saltar al contenido

En Python, ¿cuándo son dos objetos iguales?

Este equipo de trabajo ha pasado horas investigando la respuesta a tus interrogantes, te dejamos la respuesta por eso deseamos serte de mucha apoyo.

Solución:

Python tiene algunos tipos que garantiza que solo tendrán una instancia. Ejemplos de estos casos son None, NotImplemented, y Ellipsis. Estos son (por definición) singletons y cosas como None is None están garantizados para regresar True porque no hay forma de crear una nueva instancia de NoneType.

También proporciona algunos doubletons. 1True, False2 – Todas las referencias a True apuntar al mismo objeto. Nuevamente, 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á 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 asignación o no, dependiendo de sus estrategias de optimización. Algunos ejemplos que caen en esta categoría son pequeños enteros (-5 -> 255), el vacío tuple y vacio frozenset.

Finalmente, Cpython interns 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 De Verdad impar. El truco que está jugando Cpython es que siempre que construye la función foo, ve un literal de tupla 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 asunto es inmutable. Esto puede ser una gran ventaja para el rendimiento cuando el mismo circuito cerrado se repite una y otra vez. También se internan pequeñas cadenas. La verdadera victoria aquí está en las búsquedas en el diccionario. Python puede hacer una comparación de puntero (increíblemente rápida) y luego recurrir a una velocidad más lenta string comparaciones al comprobar las colisiones de hash. Dado que gran parte de Python se basa en búsquedas de diccionarios, esto puede ser una gran optimización para el lenguaje en su conjunto.


1Podría haberme inventado esa palabra … Pero espero que entiendas la idea …
2En circunstancias normales, no necesitar compruebe si el objeto es una referencia a True – Por lo general, solo te importa si el objeto es “veraz”, por ejemplo, si if some_instance: ... ejecutará la rama. Pero, puse eso aquí solo para completar.


Tenga en cuenta que is se 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 quiere comprobar si un valor es otro valor, luego use el is operador. Si quieres comprobar si un valor es igual a otro valor (pero posiblemente distinto), luego 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?
  • Comparación de Python None: ¿debería usar “es” o ==?

Apéndice

Hemos hablado de estos detalles de implementación de CPython y hemos afirmado que son optimizaciones. Sería bueno intentar medir lo que obtenemos de toda esta optimización (aparte de una pequeña confusión adicional al trabajar con el is operador).

String “pasantía” y búsquedas de diccionario.

Aquí hay una pequeña secuencia de comandos que puede ejecutar para ver cuánto más rápidas son las búsquedas en el diccionario si usa el mismo string para buscar el valor en lugar de otro string. Tenga en cuenta que utilizo el término “internado” en los nombres de las variables: estos valores no son necesariamente internos (aunque podrían estarlo). Solo lo estoy usando para indicar que el “internado” string es los string en el 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 cadenas cortas muestran aproximadamente 1 parte en 7 más rápido. los largo Las cadenas son casi 2 veces más rápidas (porque el string la comparación lleva más tiempo si el string tiene más personajes para comparar). Las diferencias no son tan sorprendentes en python3.x, pero definitivamente siguen ahí.

Tupla “pasantía”

Aquí hay un pequeño guión 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 cronometrar (y estoy feliz de tener mejores ideas sobre cómo cronometrarlo en los comentarios). La esencia de esto es que, en promedio (y en mi computadora), una tupla tarda aproximadamente un 60% en crearse como lo hace una lista. Sin embargo, foo_tuple() toma en promedio alrededor del 40% del tiempo que foo_list() acepta. Eso demuestra que realmente ganamos un poco de aceleración con 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 “pasantía”. En realidad no lo es (al menos no en el mismo sentido en que se internan las cuerdas). Podemos ver la diferencia en este sencillo 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 “internadas”: diferentes invocaciones que utilizan la misma notación literal devuelven el mismo objeto. La tupla “internación” parece ser específica de una sola línea.

Varía según la implementación.

CPython almacena en caché algunos objetos inmutables en la memoria. Este es true de enteros “pequeños” como 1 y 2 (-5 a 255, como se indica en los comentarios a continuación). CPython hace esto por razones de rendimiento; Los números enteros pequeños se usan comúnmente en la mayoría de los programas, por lo que ahorra memoria tener solo una copia creada (y es seguro porque los números enteros son inmutables).

Esto también es true de objetos “singleton” como None; solo hay uno None en existencia en un momento dado.

Otros objetos (como la tupla vacía, ()) pueden implementarse como singleton, o pueden no serlo.

En general, no debería necesariamente asumir que los objetos inmutables se implementarán de esta manera. CPython lo hace por razones de rendimiento, pero es posible que otras implementaciones no lo hagan, y es posible que CPython incluso deje de hacerlo en algún momento en el futuro. (La única excepción podría ser None, como x is None es un modismo común de Python y es probable que se implemente en diferentes intérpretes y versiones).

Por lo general, quieres usar == en lugar de is. Python is El operador no se usa con frecuencia, excepto cuando se verifica si una variable es None.

Si tienes alguna incertidumbre o forma de acrecentar nuestro enunciado puedes añadir un comentario y con deseo lo ojearemos.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *