¿Por qué el operador `is` se comporta de manera diferente en un script frente al REPL?

En Python, dos códigos tienen diferentes resultados:

a = 300 b = 300 print (a==b) print (a is b) ## print True print ("id(a) = %d, id(b) = %d"%(id(a), id(b))) ## They have same address 

Pero en modo shell (modo interactivo):

 >>> a = 300 >>> b = 300 >>> a is b False >>> id(a) 4501364368 >>> id(b) 4501362224 

El operador “is” tiene diferentes resultados.

Cuando ejecuta el código en un script .py , el archivo completo se comstack en un objeto de código antes de ejecutarlo. En este caso, CPython puede realizar ciertas optimizaciones, como reutilizar la misma instancia para el entero 300.

También puede reproducir eso en el REPL, ejecutando código en un contexto que se asemeje más a la ejecución de un script:

 >>> source = """\ ... a = 300 ... b = 300 ... print (a==b) ... print (a is b)## print True ... print ("id(a) = %d, id(b) = %d"%(id(a), id(b))) ## They have same address ... """ >>> code_obj = compile(source, filename="myscript.py", mode="exec") >>> exec(code_obj) True True id(a) = 140736953597776, id(b) = 140736953597776 

Algunas de estas optimizaciones son bastante agresivas. Podría modificar la línea de script b = 300 cambiándola a b = 150 + 150 , y CPython todavía “doblaría” b en la misma constante. Si está interesado en tales detalles de implementación, busque en “peephole.c y Ctrl + F” la tabla de consts “.

Por el contrario, cuando ejecuta el código línea por línea directamente en el REPL, se ejecuta en un contexto diferente. Cada línea se comstack en modo “único” y esta optimización no está disponible.

 >>> scope = {} >>> lines = source.splitlines() >>> for line in lines: ... code_obj = compile(line, filename="", mode="single") ... exec(code_obj, scope) ... True False id(a) = 140737087176016, id(b) = 140737087176080 >>> scope['a'], scope['b'] (300, 300) >>> id(scope['a']), id(scope['b']) (140737087176016, 140737087176080) 

En realidad, hay dos cosas que debe saber acerca de CPython y su comportamiento aquí. Primero, los enteros pequeños en el rango de [-5, 256] se internan internamente. Por lo tanto, cualquier valor que caiga en ese rango compartirá la misma ID, incluso en el REPL:

 >>> a = 100 >>> b = 100 >>> a is b True 

Desde 300> 256, no está siendo internado:

 >>> a = 300 >>> b = 300 >>> a is b False 

En segundo lugar, es que en un script, los literales se colocan en una sección constante del código comstackdo. Python es lo suficientemente inteligente como para darse cuenta de que tanto a como b refieren al literal 300 y que 300 es un objeto inmutable, puede seguir adelante y hacer referencia a la misma ubicación constante. Si ajustas un poco tu guión y lo escribes como:

 def foo(): a = 300 b = 300 print(a==b) print(a is b) print("id(a) = %d, id(b) = %d" % (id(a), id(b))) import dis dis.disassemble(foo.__code__) 

La parte inicial de la salida se ve así:

 2 0 LOAD_CONST 1 (300) 2 STORE_FAST 0 (a) 3 4 LOAD_CONST 1 (300) 6 STORE_FAST 1 (b) ... 

Como puede ver, CPython está cargando la a y b utilizando la misma ranura constante. Esto significa que b ahora se refieren al mismo objeto (porque hacen referencia a la misma ranura) y es por eso que a is b es True en el script pero no en el REPL.

También puede ver este comportamiento en el REPL, si ajusta sus declaraciones en una función:

 >>> import dis >>> def foo(): ... a = 300 ... b = 300 ... print(a==b) ... print(a is b) ... print("id(a) = %d, id(b) = %d" % (id(a), id(b))) ... >>> foo() True True id(a) = 4369383056, id(b) = 4369383056 >>> dis.disassemble(foo.__code__) 2 0 LOAD_CONST 1 (300) 2 STORE_FAST 0 (a) 3 4 LOAD_CONST 1 (300) 6 STORE_FAST 1 (b) # snipped... 

En pocas palabras: mientras que CPython realiza estas optimizaciones a veces, realmente no debería contar con eso, es realmente un detalle de implementación, y uno que ha cambiado con el tiempo (CPython solía hacer esto solo para enteros de hasta 100, para ejemplo). Si estás comparando números, usa == . 🙂