Sobre el cambio de id de una cadena inmutable

Algo sobre la id de los objetos de tipo str (en Python 2.7) me desconcierta. El tipo str es inmutable, así que espero que una vez creado, siempre tendrá el mismo id . Creo que no me expreso tan bien, así que publicaré un ejemplo de secuencia de entrada y salida.

 >>> id('so') 140614155123888 >>> id('so') 140614155123848 >>> id('so') 140614155123808 

así que mientras tanto, cambia todo el tiempo. Sin embargo, después de tener una variable que apunta a esa cadena, las cosas cambian:

 >>> so = 'so' >>> id('so') 140614155123728 >>> so = 'so' >>> id(so) 140614155123728 >>> not_so = 'so' >>> id(not_so) 140614155123728 

Así que parece que congela el id, una vez que una variable mantiene ese valor. De hecho, después de del so y del not_so , la salida de id('so') comienza a cambiar nuevamente.

Este no es el mismo comportamiento que con los enteros (pequeños).

Sé que no hay una conexión real entre la inmutabilidad y tener la misma id ; aún así, estoy tratando de averiguar la fuente de este comportamiento. Creo que alguien que esté familiarizado con las partes internas de python se sorprendería menos que yo, así que estoy tratando de llegar al mismo punto …

Actualizar

Intentar lo mismo con una cadena diferente dio diferentes resultados …

 >>> id('hello') 139978087896384 >>> id('hello') 139978087896384 >>> id('hello') 139978087896384 

Ahora es igual …

CPython no promete internar cadenas por defecto, pero en la práctica, muchos lugares en el código base de Python reutilizan los objetos de cadena ya creados. Una gran cantidad de elementos internos de Python usan (el equivalente en C de) la función intern() llama a cadenas de Python explícitamente internas, pero a menos que golpee uno de esos casos especiales, dos literales de cadena de Python idénticos producirán cadenas diferentes.

Python también tiene la libertad de reutilizar las ubicaciones de memoria, y Python también optimizará los literales inmutables almacenándolos una vez, en tiempo de comstackción, con el código de bytes en los objetos de código. El Python REPL (intérprete interactivo) también almacena el resultado de la expresión más reciente en el nombre _ , lo que confunde las cosas un poco más.

Como tal, verás que la misma identificación aparece de vez en cuando.

Ejecutar solo el id() línea id() en el REPL pasa por varios pasos:

  1. La línea se comstack, lo que incluye la creación de una constante para el objeto de cadena:

     >>> compile("id('foo')", '', 'single').co_consts ('foo', None) 

    Esto muestra las constantes almacenadas con el código de bytes comstackdo; en este caso, una cadena 'foo' y el singleton None .

  2. En la ejecución, la cadena se carga desde las constantes de código, e id() devuelve la ubicación de la memoria. El valor int resultante está enlazado a _ , así como impreso:

     >>> import dis >>> dis.dis(compile("id('foo')", '', 'single')) 1 0 LOAD_NAME 0 (id) 3 LOAD_CONST 0 ('foo') 6 CALL_FUNCTION 1 9 PRINT_EXPR 10 LOAD_CONST 1 (None) 13 RETURN_VALUE 
  3. Nada hace referencia al objeto de código, el recuento de referencias cae a 0 y el objeto de código se elimina. Como consecuencia, también lo es el objeto cadena.

Python puede entonces reutilizar la misma ubicación de memoria para un nuevo objeto de cadena, si vuelve a ejecutar el mismo código. Por lo general, esto hace que se imprima la misma dirección de memoria si repite este código. Esto depende de lo que hagas con tu memoria de Python .

La reutilización de ID no es predecible; Si mientras tanto el recolector de basura se ejecuta para borrar referencias circulares, se podría liberar otra memoria y obtendrá nuevas direcciones de memoria.

A continuación, el comstackdor de Python también internará cualquier cadena de Python almacenada como una constante, siempre que se vea lo suficiente como un identificador válido. La función de fábrica de objetos de código de Python PyCode_New internará cualquier objeto de cadena que contenga solo letras ASCII, dígitos o guiones bajos:

 /* Intern selected string constants */ for (i = PyTuple_Size(consts); --i >= 0; ) { PyObject *v = PyTuple_GetItem(consts, i); if (!PyString_Check(v)) continue; if (!all_name_chars((unsigned char *)PyString_AS_STRING(v))) continue; PyString_InternInPlace(&PyTuple_GET_ITEM(consts, i)); } 

Ya que creó cadenas que se ajustan a ese criterio, están internadas, por lo que usted ve que se usa la misma ID para la cadena 'so' en su segunda prueba: siempre que una referencia a la versión internada sobreviva, la internación causará futuro 'so' literales para reutilizar el objeto de cadena internado, incluso en nuevos bloques de código y enlazados a diferentes identificadores. En su primera prueba, no guarda una referencia a la cadena, por lo que las cadenas internadas se descartan antes de poder reutilizarse.

Por cierto, su nuevo nombre, so = 'so' une una cadena a un nombre que contiene los mismos caracteres . En otras palabras, está creando un global cuyo nombre y valor son iguales. Como Python interna tanto los identificadores como las constantes calificadas, terminas usando el mismo objeto de cadena tanto para el identificador como para su valor:

 >>> compile("so = 'so'", '', 'single').co_names[0] is compile("so = 'so'", '', 'single').co_consts[0] True 

Si crea cadenas que no son constantes de objetos de código, o contienen caracteres fuera de las letras + números + rango de subrayado, verá que el valor de id() no se reutiliza:

 >>> some_var = 'Look ma, spaces and punctuation!' >>> some_other_var = 'Look ma, spaces and punctuation!' >>> id(some_var) 4493058384 >>> id(some_other_var) 4493058456 >>> foo = 'Concatenating_' + 'also_helps_if_long_enough' >>> bar = 'Concatenating_' + 'also_helps_if_long_enough' >>> foo is bar False >>> foo == bar True 

El optimizador de mirilla de Python hace un cálculo previo de los resultados de expresiones simples, pero si esto resulta en una secuencia de más de 20, la salida se ignora (para evitar el uso de memoria y objetos de código). por lo tanto, la concatenación de cadenas más cortas que consisten solo en caracteres de nombre puede llevar a cadenas internadas si el resultado es de 20 caracteres o menos.

Este comportamiento es específico del shell interactivo de Python. Si pongo lo siguiente en un archivo .py:

 print id('so') print id('so') print id('so') 

y ejecutarlo, recibo la siguiente salida:

 2888960
 2888960
 2888960

En CPython, una cadena literal se trata como una constante, que podemos ver en el bytecode del fragmento de código anterior:

  2 0 LOAD_GLOBAL 0 (id) 3 LOAD_CONST 1 ('so') 6 CALL_FUNCTION 1 9 PRINT_ITEM 10 PRINT_NEWLINE 3 11 LOAD_GLOBAL 0 (id) 14 LOAD_CONST 1 ('so') 17 CALL_FUNCTION 1 20 PRINT_ITEM 21 PRINT_NEWLINE 4 22 LOAD_GLOBAL 0 (id) 25 LOAD_CONST 1 ('so') 28 CALL_FUNCTION 1 31 PRINT_ITEM 32 PRINT_NEWLINE 33 LOAD_CONST 0 (None) 36 RETURN_VALUE 

La misma constante (es decir, el mismo objeto de cadena) se carga 3 veces, por lo que los ID son los mismos.

En su primer ejemplo 'so' se crea una nueva instancia de la cadena 'so' cada vez, por lo tanto, una identificación diferente.

En el segundo ejemplo, está vinculando la cadena a una variable y Python puede mantener una copia compartida de la cadena.

Entonces, aunque no se garantiza que Python interne cadenas, con frecuencia reutilizará la misma cadena, y puede inducir a error. Es importante saber que no debe verificar la id o is para la igualdad de cadenas.

Para demostrar esto, una forma he descubierto forzar una nueva cadena en Python 2.6 al menos:

 >>> so = 'so' >>> new_so = '{0}'.format(so) >>> so is new_so False 

Y aquí hay un poco más de exploración de Python:

 >>> id(so) 102596064 >>> id(new_so) 259679968 >>> so == new_so True 

Una forma más simplificada de entender el comportamiento es verificar los siguientes tipos de datos y variables .

La sección “Pecularidad de una cadena” ilustra su pregunta utilizando caracteres especiales como ejemplo.