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="")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)Pedido 1: 12.50 €Pedido 2: 12.50 €Pedido 3: 12.50 €parts = []es una lista vacía.appendañ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
\nal 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
- report apunta a una cadena vacía.
- El primer += crea un str nuevo con una línea: 18 caracteres.
- 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.
- 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.
- parts es una lista vacía: en ella irán los trozos.
- append mete cada trozo en la lista en su sitio. Los trozos viejos no se copian, y la lista sigue siendo la misma.
- 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, usajoin. - 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])4532 0151 1283 0366**** **** **** 03660888123456amorcard[: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 = notecopy.append(", 2 cafés")print("".join(note))
text = "Mesa 4"other = textother += ", 2 cafés"print(text)copy = note copia la flecha, las dos variables apuntan a la misma lista, y append la cambia en su sitio. Con una cadena es distinto: other += ... crea una cadena nueva y mueve solo other hacia ella. text sigue apuntando a la vieja.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())TrueTrueFalseTruea < besTruesiava antes queb;a == b: son iguales;a > b:ava después deb.
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] = tmpprint(", ".join(names))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.ordda 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
compareToen 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))32Alonso, Blanco, Iglesias, TorresPrué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:]))El texto se compara carácter por carácter. Los cuatro primeros caracteres son iguales, después vienen «1» y «9», y «1» va antes que «9». Por eso "INV-10" va antes que "INV-9", igual que en el diccionario «ba» va antes que «c». Los números 10 y 9 se comparan como números solo después de int(). Si ordenas números como texto, rellénalos con ceros: INV-009, INV-010.
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)lucia.gomezempresa.es19Lucí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, yfind(".", at)empieza a buscar solo desde el índiceaten adelante.rfindbusca desde atrás. Si no hay ese carácter,finddevuelve -1.email[:at]es un slice: desde el principio hastaat, sin él.email[at + 1:]es desdeat + 1hasta el final.joines lo contrario desplit."=" * 20repite el texto 20 veces.
Vacío y «en blanco» no son lo mismo:
empty = ""spaces = " "print(empty == "", spaces == "")print(empty.strip() == "", spaces.strip() == "")True FalseTrue 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
indexbusca comofind, pero cuando no hay ese carácter detiene el programa conValueErroren lugar de devolver -1.isspace()parece la comprobación que necesitas, pero para un texto vacío esFalse: 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)-1False Truesubstring not foundLa 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)42-2.4int() 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 = 0for 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))Café 2 x 2.40 = 4.80Cruasán 1 x 3.10 = 3.10Agua 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 conValueError: 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}")could not convert string to float: '2,40'2.40Prué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.
reversed(word) no devuelve una cadena. Devuelve un objeto que da los caracteres de atrás hacia delante, uno por uno. == entre una cadena y un objeto así es False, tenga lo que tenga dentro. La corrección es "".join(reversed(word)), o más corto, word[::-1].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.
Anita lava la tinaPalíndromoAnita lava la ropaNo es un palíndromo2. 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.
3Pan;1.20;2Queso;9.80;1Huevos;0.35;10Pan 2 x 1.20 = 2.40Queso 1 x 9.80 = 9.80Huevos 10 x 0.35 = 3.50------------------------------------Total 15.703. 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.
Mañana a las 9 hay reuniónreunión hay 9 las a Mañanasplit() 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.escreal**********@empresa.es: la primera letra y después tantos asteriscos como caracteres quedan antes de la@. Usafindy*. - 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 conis, toma el slice[0:5], divide la segunda por el espacio e imprime las partes, convierte"100"en un número e imprímelo conf"Valor: {...}", y al final añade" y más"a una lista con la primera cadena y pégalas conjoin. 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
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Cadenas: trabajar con texto · Clases y objetos · Métodos de instancia · Herencia
- Continúa en: Expresiones regulares · Expresiones regulares: grupos y reemplazo · Archivos · Datos en archivos
- Ejercicios sobre el tema: Texto y archivos