Guía para la pasantía de cadenas en Python

En este artículo, nos sumergiremos en el concepto de String Interning en Python. Con ejemplos y teoría, explicaremos cómo funciona y qué beneficios aporta a las tareas cotidianas.

Introducción

Una de las primeras cosas que encuentra al aprender los conceptos básicos de la programación es el concepto de cadenas. Al igual que varios lenguajes de programación, las cadenas de Python son matrices de bytes que representan caracteres Unicode: una matriz o secuencia de caracteres. Python, a diferencia de muchos lenguajes de programación, no tiene un tipo de datos de carácter distinto, y los caracteres se consideran cadenas de longitud 1.

Puede definir una cadena usando comillas simples o dobles, por ejemplo, a = "Hola mundo" o a = 'Hola mundo'. Para acceder a un elemento específico de una cadena, usaría corchetes ([]) con el índice del carácter al que desea acceder (la indexación comienza en 0). Llamar a a[0], por ejemplo, devolvería H.

Dicho esto, echemos un vistazo a este ejemplo de código:

1
2
3
4
5
6
7
8
a = 'Hello World'
b = 'Hello World'
c = 'Hello Worl'

print(a is b)
print(a == b)
print(a is c+'d')
print(a == c+'d')

Todas las cadenas que comparamos tienen el valor de Hello World (a, b y c +'d'). Podría ser intuitivo suponer que la salida sería Verdadero para todas estas declaraciones.

Sin embargo, cuando ejecutamos el código, da como resultado:

1
2
3
4
True
True
False
True

Lo que puede parecer poco intuitivo sobre esta salida es que a is c + 'd' devuelve False, mientras que una declaración muy similar a is b devuelve True. Con esto, podemos concluir que a y b son el mismo objeto, mientras que c es diferente, aunque tengan el mismo valor.

Si no está familiarizado con la Diferencia entre == y es - is comprueba si las variables se refieren al mismo objeto en memoria, mientras que == comprueba si las variables tienen el mismo valor.

Esta distinción entre a, b y c es el producto de String Interning.

Nota: El entorno en el que ejecuta el código afecta el funcionamiento de la internación de cadenas. Los ejemplos anteriores fueron el resultado de ejecutar el código como un script en un entorno no interactivo, utilizando la última versión actual de Python (versión 3.8.5). El comportamiento será diferente al usar la consola/Jupyter debido a las diferentes formas en que se optimiza el código, o incluso entre las diferentes versiones de Python.

Esto se debe a que diferentes entornos tienen diferentes niveles de optimización.

Internamiento de cadenas {#internamiento de cadenas}

Las cadenas son objetos inmutables en Python. Esto significa que una vez que se crean las cadenas, no podemos cambiarlas ni actualizarlas. Incluso si parece que se ha modificado una cadena, bajo el capó, se creó una copia con el valor modificado y se asignó a la variable, mientras que la cadena original permaneció igual.

Intentemos modificar una cadena:

1
2
name = 'Wtack Abuse!'
name[0] = 'S'

Como la cadena name es inmutable, este código fallará en la última línea:

1
2
name[0] = 'S'
TypeError: 'str' object does not support item assignment

Nota: Si realmente desea cambiar un carácter particular de una cadena, puede convertir la cadena en un objeto mutable como una lista y cambiar el elemento deseado:

1
2
3
4
5
6
7
name = 'Wtack Abuse!'
name = list(name)
name[0] = 'S'
# Converting back to string
name = "".join(name) 

print(name)

Lo que nos da la salida deseada:

1
Stack Abuse!

La razón por la que podemos cambiar el carácter en la lista (y no en la cadena) es porque las listas son mutables, lo que significa que podemos cambiar sus elementos.

String Interning es un proceso de almacenar solo una copia de cada valor de cadena distinto en la memoria.

Esto significa que, cuando creamos dos cadenas con el mismo valor, en lugar de asignar memoria para ambas, solo una cadena se compromete en la memoria. El otro simplemente apunta a esa misma ubicación de memoria.

Dada esta información, volvamos al ejemplo inicial de Hello World:

1
2
3
a = 'Hello World'
b = 'Hello World'
c = 'Hello Worl'

Cuando se crea la cadena a, el compilador verifica si Hello World está presente en la memoria interna. Dado que es la primera aparición de este valor de cadena, Python crea un objeto y almacena en caché esta cadena en la memoria y apunta a a esta referencia.

Cuando se crea b, el compilador encuentra Hello World en la memoria interna, por lo que en lugar de crear otra cadena, b simplemente apunta a la memoria previamente asignada.

valores de cadena de python en memoria

a es b y a == b en este caso.

Finalmente, cuando creamos la cadena c = 'Hello Worl', el compilador instancia otro objeto en la memoria interna porque no pudo encontrar el mismo objeto como referencia.

Cuando comparamos a y c+'d', este último se evalúa como Hello World. Sin embargo, dado que Python no realiza internaciones durante el tiempo de ejecución, en su lugar se crea un nuevo objeto. Por lo tanto, dado que no se realizó ninguna internación, estos dos no son el mismo objeto y es devuelve Falso.

En contraste con el operador is, el operador == compara los valores de las cadenas después de calcular las expresiones runtime - Hello World == Hello World.

En ese momento, a y c+'d' tienen el mismo valor, por lo que esto devuelve True.

Verificación

Veamos el id de los objetos de cadena que creamos. La función id (objeto) en Python devuelve el ID de objeto, que se garantiza que es único durante la vida útil de dicho objeto. Si dos variables apuntan al mismo objeto, llamar a id devolvería el mismo número:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
letter_d = 'd'

a = 'Hello World'
b = 'Hello World'
c = 'Hello Worl' + letter_d
d = 'Hello Worl' + 'd'

print(f"The ID of a: {id(a)}")
print(f"The ID of b: {id(b)}")
print(f"The ID of c: {id(c)}")
print(f"The ID of d: {id(d)}")

Esto resulta en:

1
2
3
4
The ID of a: 16785960
The ID of b: 16785960
The ID of c: 17152424
The ID of d: 16785960

Solo c tiene una identificación diferente. Todas las referencias apuntan ahora al objeto con el mismo valor Hello World. Sin embargo, c no se calculó en tiempo de compilación, sino en tiempo de ejecución. Incluso d, que generamos agregando el carácter 'd' ahora apunta al mismo objeto al que apuntan a y b.

Cómo se internan las cadenas

En Python, hay dos formas en que las cadenas se pueden internar en función de la interacción del programador:

  • Internamiento implícito
  • Internamiento explícito

Interning implícito {#interning implícito}

Python interna automáticamente algunas cadenas en el momento de su creación. Que una cadena esté internada o no depende de varios factores:

  • Todas las cadenas vacías y las cadenas de longitud 1 están internadas.

  • Hasta la versión 3.7, Python usaba optimización de mirillas, y todas las cadenas de más de 20 caracteres no se internaban. Sin embargo, ahora usa el Optimizador AST, y (la mayoría) de cadenas de hasta 4096 caracteres están internadas.

  • Los nombres de funciones, clases, variables, argumentos, etc. están implícitamente internados.

  • Las claves de los diccionarios utilizados para contener atributos de módulos, clases o instancias están internadas.

  • Las cadenas se internan solo en tiempo de compilación, lo que significa que no se internarán si su valor no se puede calcular en tiempo de compilación.

    • These strings will be interned for example:
    1
    2
    
    a = 'why'
    b = 'why' * 5
    
    • The following expression is computed at runtime thus the string is not interned.
    1
    
    b = "".join(['w','h','y'])
    
  • Es muy probable que las cadenas que tengan caracteres distintos de ASCII no se internen.

Si recuerda, dijimos que 'Hello Worl' + letter_d se calculaba en tiempo de ejecución y, por lo tanto, no se internaría. Dado que no existe un estándar consistente en la internación de cadenas, una buena regla general para usar es la idea de tiempo de compilación/tiempo de ejecución, donde puede suponer que una cadena se internará si se puede calcular en tiempo de compilación.

Prácticas explícitas {#internaciones explícitas}

A menudo nos encontramos con cadenas que no se encuentran bajo las condiciones de internamiento implícito en Python, pero hay una manera de internar cualquier cadena que desee. Hay una función en el módulo sys llamada intern(immutable_object), esta función le dice a Python que almacene el immutable_object (cadena en nuestro caso) en la tabla de memoria interna.

Puede internar cualquier tipo de cadena de la siguiente manera:

1
2
import sys
c = sys.intern('Hello World'+'!')

Podemos ver que esto funcionaría en nuestro ejemplo anterior:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import sys

letter_d = 'd'

a = sys.intern('Hello World')
b = sys.intern('Hello Worl' + letter_d)

print(f"The ID of a: {id(a)}")
print(f"The ID of b: {id(b)}")
print(f"a is b? {a is b}")

Daría la salida:

1
2
3
The ID of a: 26878464
The ID of b: 26878464
a is b? True

Ahora que sabemos cómo y qué cadenas se internan en Python. Queda una pregunta: ¿por qué se introdujo la pasantía en cadenas?

Ventajas de String Interning

String internating tiene varias ventajas:

  • Guardar memoria: Nunca tenemos que guardar dos objetos de cadena en la memoria por separado si son iguales. Cada nueva variable con el mismo contenido solo apunta a la referencia en el literal de la tabla interna. Si por alguna razón quisiera tener una lista que contuviera cada palabra y su aparición en Orgullo y prejuicio de Jane Austen, sin internamiento explícito, necesitaría 4.006.559 bytes, y con interno explícito de cada palabra, solo necesitaría 785.509 bytes de memoria.
  • Comparaciones rápidas: La comparación de cadenas internas es mucho más rápida que las cadenas no internas que son útiles cuando su programa tiene muchas comparaciones. Esto sucede porque para comparar cadenas internas, solo necesita comparar si sus direcciones de memoria son las mismas, en lugar de comparar los contenidos.
  • Búsquedas rápidas de diccionario: Si las claves de búsqueda están internadas, la comparación se puede realizar mediante comparaciones de punteros en lugar de comparaciones de cadenas, que funciona según el mismo principio que el punto anterior.

Desventajas de String Interning

Sin embargo, las cadenas internas tienen algunos inconvenientes y cosas a considerar antes de usar:

  • Costo de memoria: en caso de que su programa tenga una gran cantidad de cadenas con diferentes valores y relativamente menos comparaciones en general porque la tabla interna en sí consume memoria. Lo que significa que desea internar cadenas si tiene relativamente pocas cadenas y muchas comparaciones entre ellas.
  • Costo de tiempo: La llamada a la función intern() es costosa ya que tiene que administrar la tabla interna.
  • Entornos multiproceso: La memoria interna (tabla) es un recurso global en un entorno multiproceso cuya sincronización debe modificarse. Es posible que esta verificación solo sea necesaria cuando se accede a la tabla interna, es decir, cuando se crea una nueva cadena, pero puede ser costosa.

Conclusión

Al utilizar string interning, se asegura de que solo se cree un objeto, incluso si define varias cadenas con el mismo contenido. Sin embargo, debe tener en cuenta el equilibrio entre las ventajas y desventajas de la pasantía de cadenas, y solo usarla cuando crea que su programa podría beneficiarse.

Recuerde siempre agregar comentarios o documentación si está utilizando la internación de cadenas para que otros miembros del equipo sepan cómo manejar las cadenas en el programa.

Si bien los resultados pueden variar según la implementación de su intérprete de Python, así como el entorno en el que ejecuta su código, definitivamente debería jugar con la función intern() para sentirse cómodo con ella. Este concepto puede ayudarlo a mejorar el diseño y el rendimiento de su código. También podría ayudarte en tu próxima entrevista de trabajo.

Licensed under CC BY-NC-SA 4.0