Ir al contenido
wannacode

Cadenas

En resumen: Una cadena en Python no cambia: cada += crea un texto nuevo, por eso muchos trozos se juntan con una lista y join.

Problema

El informe que se arrastra

Ya sabes que title.upper() por sí solo no hace nada: una cadena es inmutable, y el método devuelve una cadena nueva. Esto tiene un coste, y se ve en un bucle.

El programa de la tienda en línea construye el informe del día línea por línea:

report = ""
for i in range(1, 4):
report += f"Pedido {i}: 12.50 €\n"
print(report, end="")
Salida
Pedido 1: 12.50 €
Pedido 2: 12.50 €
Pedido 3: 12.50 €

Con tres pedidos es instantáneo. El sábado hay 50 000 pedidos, y el mismo bucle tarda unos 6,5 segundos. Lo medimos en la máquina donde revisamos el código de las lecciones.

¿Por qué? Cada report += ... no añade nada a la cadena vieja. Crea una cadena nueva, copia en ella todo el informe hasta ahora y solo entonces le pega la línea nueva. Hacia el final, cada vuelta copia más de un millón de caracteres.

La idea

Una lista de trozos y join

Python no tiene una cadena que cambie en su sitio. Por eso juntas los trozos en una lista, que sí cambia en su sitio. Al final, join los pega en una sola cadena y copia cada carácter una sola vez:

parts = []
for i in range(1, 4):
parts.append(f"Pedido {i}: 12.50 €")
report = "\n".join(parts)
print(report)
Salida
Pedido 1: 12.50 €
Pedido 2: 12.50 €
Pedido 3: 12.50 €
  • parts = [] es una lista vacía. append añade un trozo al final sin tocar los demás.
  • "\n".join(parts) crea una sola cadena con todos los trozos, con el separador "\n" entre ellos. El separador es el texto que va antes de .join. Para pegar sin separador, escribes "".join(parts).
  • El salto de línea entre los trozos viene del separador, por eso aquí los trozos no llevan \n al final.

Con 50 000 pedidos esta versión tarda unos 6 milisegundos en lugar de 6,5 segundos.

Esto es lo que pasa en la memoria con cada forma:

report = ""
for i in range(1, 4):
report += f"Pedido {i}: 12.50 €\n"
parts = []
for i in range(1, 4):
parts.append(f"Pedido {i}: 12.50 €\n")
joined = "".join(parts)
print(len(report), len(joined))
print(report == joined)
Paso a paso, en palabras
  1. report apunta a una cadena vacía.
  2. El primer += crea un str nuevo con una línea: 18 caracteres.
  3. El segundo += crea otro str. Copia en él la línea vieja (18 caracteres) y solo entonces le pega la nueva. La cadena vieja ya no le sirve a nadie.
  4. El tercero copia 36 caracteres. Con cada línea se copia más: con 50 000 líneas son más de un millón de caracteres por vuelta.
  5. parts es una lista vacía: en ella irán los trozos.
  6. append mete cada trozo en la lista en su sitio. Los trozos viejos no se copian, y la lista sigue siendo la misma.
  7. join pega los trozos una sola vez: 54 caracteres. El texto es el mismo. Con += se copiaron otros 54 caracteres viejos, y con join ni uno solo.
Más precisamente, para curiosos
  • El mismo bucle con += dentro de una función tarda unos 4 milisegundos. Python (CPython, la versión habitual) hace trampa: si nada más apunta a la cadena vieja, la alarga en su sitio. En el nivel superior del archivo eso no pasa. El truco no es una regla del lenguaje, así que no cuentes con él: para muchos trozos, usa join.
  • Una sola expresión como f"Pedido {i}: {price}" es una sola cadena nueva. El problema es solo += en un bucle, donde cada vuelta es una cadena nueva aparte.

Míralo

Cambiar con slices

Una cadena no cambia en su sitio, así que cada cambio es una cadena nueva, construida con partes de la vieja. Aquí tienes un número de tarjeta para imprimirlo en un recibo:

card = "4532015112830366"
card = card[:12] + " " + card[12:]
card = card[:8] + " " + card[8:]
card = card[:4] + " " + card[4:]
print(card)
card = "**** **** ****" + card[14:]
print(card)
phone = "0888-123-456"
phone = phone[:8] + phone[9:]
phone = phone[:4] + phone[5:]
print(phone)
print("roma"[::-1])
Salida
4532 0151 1283 0366
**** **** **** 0366
0888123456
amor

card[:12] es un slice: los caracteres desde el principio hasta el índice 12, sin él. card[12:] es desde el índice 12 hasta el final.

Quieres Escribes
insertar x antes del índice i s[:i] + x + s[i:]
reemplazar los caracteres desde a hasta b (sin b) por x s[:a] + x + s[b:]
quitar el carácter con índice i s[:i] + s[i + 1:]
quitar los caracteres desde a hasta b (sin b) s[:a] + s[b:]
dar la vuelta al texto, de atrás hacia delante s[::-1]
la longitud, un carácter len(s), s[i]

Los espacios del número de tarjeta se insertan de atrás hacia delante: primero en 12, después en 8, después en 4. Si empiezas por 4, el espacio insertado desplaza todo lo que hay detrás, y 8 ya no es el lugar que quieres. Por la misma razón, los guiones del teléfono se quitan primero por detrás.

Pruébalo

Dos nombres, un objeto

¿Qué imprimirá el programa?

note = ["Mesa 4"]
copy = note
copy.append(", 2 cafés")
print("".join(note))
text = "Mesa 4"
other = text
other += ", 2 cafés"
print(text)
Respuesta

La idea

En orden alfabético: < y >

== solo te dice «¿son iguales?». Para ordenar necesitas más: qué cadena va antes que la otra. En Python comparas cadenas directamente con < y >:

print("Alonso" < "Blanco")
print("Blanco" == "Blanco")
print("iglesias" < "Iglesias")
print("iglesias".casefold() == "Iglesias".casefold())
Salida
True
True
False
True
  • a < b es True si a va antes que b;
  • a == b: son iguales;
  • a > b: a va después de b.

Las mayúsculas van antes que las minúsculas, por eso "iglesias" < "Iglesias" es False. casefold() pasa el texto a minúsculas para comparar, sin importar cómo esté escrito. Con > ordenas una lista de nombres, por ejemplo con el ordenamiento de burbuja de la lección sobre arrays:

names = ["Torres", "Alonso", "Iglesias", "Blanco"]
for i in range(len(names) - 1):
for j in range(len(names) - 1 - i):
if names[j] > names[j + 1]:
tmp = names[j]
names[j] = names[j + 1]
names[j + 1] = tmp
print(", ".join(names))
Salida
Alonso, Blanco, Iglesias, Torres

", ".join(names) pega los elementos de la lista con un separador entre ellos.

Más precisamente, para curiosos
  • < y > comparan carácter por carácter, según su código en la tabla de caracteres. ord da el código de un carácter: «i» e «I» se diferencian en 32.
  • Python no tiene un método que devuelva un número negativo, 0 o uno positivo, como compareTo en Java. No lo necesitas: <, == y > te dicen lo mismo.
  • names.sort() ordena la lista con la misma regla, sin escribir el ordenamiento de burbuja.
print(ord("i") - ord("I"))
names = ["Torres", "Alonso", "Iglesias", "Blanco"]
names.sort()
print(", ".join(names))
Salida
32
Alonso, Blanco, Iglesias, Torres

Pruébalo

¿Factura 10 o 9?

Los números de las facturas son texto. ¿Cuál va antes?

¿Qué imprimirá el programa?

a = "INV-10"
b = "INV-9"
print(a < b)
print(int(a[4:]) < int(b[4:]))
Respuesta

La idea

Buscar, cortar, juntar

Aquí tienes una línea de un archivo de contactos: nombre, correo y ciudad, separados con « ; », con espacios de sobra en los extremos:

line = " Lucía Gómez ; lucia.gomez@empresa.es ; Sevilla "
parts = line.strip().split(" ; ")
email = parts[1]
at = email.find("@")
print(email[:at])
print(email[at + 1:])
print(email.find(".", at))
print(" | ".join(parts))
print("=" * 20)
Salida
lucia.gomez
empresa.es
19
Lucía Gómez | lucia.gomez@empresa.es | Sevilla
====================
  • strip() quita los espacios de delante y de detrás, y también los espacios «anchos» de otros idiomas.
  • find("@") da el índice de la primera aparición, y find(".", at) empieza a buscar solo desde el índice at en adelante. rfind busca desde atrás. Si no hay ese carácter, find devuelve -1.
  • email[:at] es un slice: desde el principio hasta at, sin él. email[at + 1:] es desde at + 1 hasta el final. join es lo contrario de split.
  • "=" * 20 repite el texto 20 veces.

Vacío y «en blanco» no son lo mismo:

empty = ""
spaces = " "
print(empty == "", spaces == "")
print(empty.strip() == "", spaces.strip() == "")
Salida
True False
True True

== "" es verdadero solo para un texto de longitud 0. strip() == "" también es verdadero cuando solo hay espacios. Para revisar un campo que alguien ha rellenado, normalmente quieres lo segundo.

Más precisamente, para curiosos
  • index busca como find, pero cuando no hay ese carácter detiene el programa con ValueError en lugar de devolver -1.
  • isspace() parece la comprobación que necesitas, pero para un texto vacío es False: es verdadero solo cuando hay al menos un carácter y todos son espacios.
email = "lucia.gomez@empresa.es"
print(email.find("#"))
print("".isspace(), " ".isspace())
try:
email.index("#")
except ValueError as e:
print(e)
Salida
-1
False True
substring not found

La idea

Números y texto

Los números entran en el texto y salen del texto:

code = int("0042")
price = float("2.40")
label = str(code) + "-" + str(price)
print(label)
Salida
42-2.4

int() y float() ya los conoces. str() es lo contrario: crea texto a partir de un número o de un bool.

Para una tabla con columnas necesitas una f-string con un ancho para cada columna. Aquí tienes un recibo de una cafetería:

names = ["Café", "Cruasán", "Agua mineral"]
prices = [2.40, 3.10, 1.50]
qty = [2, 1, 3]
total = 0
for i in range(len(names)):
amount = prices[i] * qty[i]
total += amount
print(f"{names[i]:<16}{qty[i]:2d} x {prices[i]:5.2f} = {amount:6.2f}")
print("-" * 35)
print("{:<26}{:9.2f} €".format("Total", total))
Salida
Café 2 x 2.40 = 4.80
Cruasán 1 x 3.10 = 3.10
Agua mineral 3 x 1.50 = 4.50
-----------------------------------
Total 12.40 €
  • {names[i]:<16} es un texto en 16 caracteres, alineado a la izquierda. {prices[i]:5.2f} es un número en 5 caracteres con 2 después del punto, alineado a la derecha. {qty[i]:2d} es un número entero en 2 caracteres.
  • "plantilla".format(...) hace lo mismo con {} vacías en la plantilla: los valores vienen de fuera, en orden. Es útil cuando la plantilla está lista de antes y los valores llegan después.
Más precisamente, para curiosos
  • El ancho es como mínimo esa cantidad de caracteres. Un texto más largo no se corta: empuja la columna hacia la derecha.
  • float("2,40") se detiene con ValueError: el separador decimal es siempre un punto. Y la f-string siempre escribe un punto, sea cual sea la configuración de la computadora.
try:
float("2,40")
except ValueError as e:
print(e)
print(f"{2.4:.2f}")
Salida
could not convert string to float: '2,40'
2.40

Pruébalo

Encuentra el error

«Reconocer» se lee igual al revés, pero el programa dice «reconocer no es un palíndromo». ¿En qué línea está el error?

Elige la línea con el error.

Ejercicio

Texto que tú juntas

Resuelve las tareas en tu editor. No imprimas preguntas.

1. Palíndromo. Lee una línea y di si es un palíndromo, sin tener en cuenta las mayúsculas, los espacios, las comas y los guiones. Esta vez da la vuelta al texto con el slice [::-1] en lugar de con un bucle.

Entrada
Anita lava la tina
Salida
Palíndromo
Entrada
Anita lava la ropa
Salida
No es un palíndromo

2. Un recibo con columnas. La primera línea tiene el número de artículos. Cada línea siguiente tiene un nombre, un precio y una cantidad, separados con ;. Imprime un recibo con las columnas alineadas y el total al final.

Entrada
3
Pan;1.20;2
Queso;9.80;1
Huevos;0.35;10
Salida
Pan 2 x 1.20 = 2.40
Queso 1 x 9.80 = 9.80
Huevos 10 x 0.35 = 3.50
------------------------------------
Total 15.70

3. Palabras al revés. Lee una frase e imprime sus palabras en orden inverso. Entre las palabras puede haber más de un espacio, y en el resultado tiene que haber exactamente uno. Júntalo con una lista y join.

Entrada
Mañana a las 9 hay reunión
Salida
reunión hay 9 las a Mañana

split() sin argumento divide por cada grupo de espacios seguidos y no da partes vacías. Prueba también split(" ") para ver la diferencia.

Más tareas

  • Iniciales. Lee un nombre completo («María López Sánchez») e imprime las iniciales con puntos: «M. L. S.» Júntalas con una lista y join.
  • Correo oculto. A partir de lucia.gomez@empresa.es crea l**********@empresa.es: la primera letra y después tantos asteriscos como caracteres quedan antes de la @. Usa find y *.
  • Un recorrido por str. Crea una cadena con comillas ("Texto de ejemplo") y otra pegada a partir de dos partes con "".join(["Texto", " de ejemplo"]). Compáralas con == y con is, toma el slice [0:5], divide la segunda por el espacio e imprime las partes, convierte "100" en un número e imprímelo con f"Valor: {...}", y al final añade " y más" a una lista con la primera cadena y pégalas con join. Predice cada línea antes de ejecutar el programa.
  • Ordenar sin distinguir mayúsculas. Cambia el ordenamiento de burbuja de la tarjeta sobre comparar para que compare el casefold() de las dos cadenas, y pruébalo con «lucas», «Bruno» y «ana».

Test

Ponte a prueba

1. ¿Por qué report += line es lento en un bucle con 50 000 vueltas en el nivel superior del archivo?
2. ¿Qué da "Blanco" > "Alonso"?
3. a = ["12"]; b = a; b.append("3"). ¿Qué da "".join(a)?
4. parts es una lista de cadenas. ¿Cómo haces con ella una sola cadena?
5. ¿Qué diferencia hay entre f"{total:6.2f}" y "{:6.2f}".format(total)?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·