Strings en Python — práctica limpiando una lista de clase, validando DNIs y sacando un acta alineada (y el 7,5 que float() no quiso leer)
Con la lista de clase ya limpia (y aprendida la lección de guardar lo que devuelve strip()), me pasaron las notas de los parciales. Venían de una hoja de cálculo en español, así que cada nota estaba escrita con coma decimal. Las copié a mi programa y fui directo a convertirlas en números:
nota = float("7,5") # así venía la nota en la hoja de cálculo
print(nota)
Al ejecutarlo:
Traceback (most recent call last):
File "gancho.py", line 1, in <module>
nota = float("7,5") # así venía la nota en la hoja de cálculo
ValueError: could not convert string to float: '7,5'
«¿Cómo que no puede convertir 7,5? Si es un número.» Para mí, sí. Para Python, "7,5" es un string con un 7, una coma y un 5, y float() solo entiende el punto como separador decimal. La solución es de una línea, float(texto.replace(",", ".")), pero la lección es más grande: los datos que escriben las personas nunca llegan limpios. Espacios de más, mayúsculas al azar, comas donde Python espera puntos, fechas con una cifra… Antes de calcular nada, hay que limpiar y comprobar.
Eso es justo lo que vamos a practicar con tres programas completos sobre una lista de clase: limpiar los nombres escritos de cualquier manera y generar el usuario de cada alumno, validar DNIs y fechas de nacimiento explicando qué falla en cada una, y sacar el acta de los parciales con las columnas perfectamente alineadas. Los tres son de nivel medio y usan datos que se parecen a los de verdad, con sus huecos y sus errores. Esta práctica se apoya en la teoría de strings; si algo de los diccionarios te suena raro, repasa también contenedores asociativos, porque aparecen en dos de los programas.
Tabla de Contenidos
Cómo usar este artículo
Cada programa está completo en un solo archivo. Cópialo, guárdalo con el nombre que se indica y lánzalo desde la terminal con python3 nombre.py. Debajo de cada uno tienes la salida real, un dibujo o una tabla con lo que pasa por dentro y una explicación función a función. Antes de mirar la salida, intenta predecirla: con los strings, saber qué devuelve cada método (y qué no cambia) es exactamente lo que hay que entrenar.
Conviene tener claro, de la teoría:
- Que los strings son inmutables: cada método devuelve uno nuevo que hay que guardar.
- La diferencia entre
split()ysplit(" "), y cómojoinvuelve a pegar los trozos. - Cómo funcionan los índices y los slices (
dni[:8],dni[8]). - Los modificadores de formato de las f-strings: ancho, alineación y
.2f.
Ningún programa usa input(). Los datos van fijos en el código para que tu salida sea idéntica a la que ves aquí. Si quieres probar con tus propios datos, cambia las listas del principio de cada programa.
Programa 1 · Limpiar la lista de clase
La lista llega de un formulario en el que cada alumno escribió «apellidos, nombre» a su manera: espacios delante y detrás, varios espacios seguidos, la coma pegada o separada, mayúsculas por todas partes. Queremos dos cosas para cada alumno: su nombre bien escrito (María José Núñez de la Torre) y un usuario para el campus virtual con la forma nombre.apellido, en minúsculas y sin tildes.
La idea
Antes de programar, piensa en el orden. Primero hay que separar apellidos y nombre (por la coma), después limpiar cada parte (espacios y mayúsculas) y, solo al final, construir el usuario a partir de las partes ya limpias. Cada paso es una función pequeña que recibe un string y devuelve otro: así se pueden probar por separado y combinarlas como piezas.
La coma puede tener espacios a cualquier lado ("torre , MARÍA", "SANTANA,luis"). Si partes por ", " (coma y espacio), la segunda línea no se parte. Mejor parte solo por la coma y limpia los espacios después.
limpiar.py
# Lista tal como llegó del formulario: "apellidos, nombre", escrita por cada uno a su manera
CRUDA = [
" gARCÍA pérez, ana ",
"ROMERO SANTANA,luis",
"núñez de la torre , MARÍA JOSÉ",
"gil, marta ",
]
PARTICULAS = ["de", "la", "del", "las", "los", "y"]
TILDES = {"á": "a", "é": "e", "í": "i", "ó": "o", "ú": "u", "ü": "u", "ñ": "n"}
def normalizar_espacios(texto):
"""Quita los espacios de los extremos y deja UN solo espacio entre palabras."""
return " ".join(texto.split()) # split() sin argumento corta por cualquier hueco
def corregir_particulas(texto):
"""title() escribe 'De La'; las partículas van en minúscula (salvo si son la primera palabra)."""
palabras = texto.split()
for i in range(1, len(palabras)): # desde la 1: la primera palabra no se toca
if palabras[i].lower() in PARTICULAS:
palabras[i] = palabras[i].lower() # en una LISTA sí se puede cambiar un elemento
return " ".join(palabras)
def nombre_bonito(linea):
"""'gARCÍA pérez, ana' -> ('Ana', 'García Pérez')."""
apellidos, coma, nombre = linea.partition(",") # corta por la PRIMERA coma
nombre = normalizar_espacios(nombre).title()
apellidos = corregir_particulas(normalizar_espacios(apellidos).title())
return nombre, apellidos
def sin_tildes(texto):
"""Cambia cada vocal con tilde (y la ñ) por su versión sin tilde."""
for con, sin in TILDES.items():
texto = texto.replace(con, sin) # replace devuelve un string nuevo: hay que guardarlo
return texto
def usuario(nombre, apellidos):
"""Primera palabra del nombre + '.' + primer apellido, en minúsculas y sin tildes."""
primero = nombre.split()[0]
apellido1 = apellidos.split()[0]
return sin_tildes((primero + "." + apellido1).lower()) # primero lower: TILDES está en minúsculas
for n, linea in enumerate(CRUDA, start=1):
nombre, apellidos = nombre_bonito(linea)
print(f"{n}. {nombre} {apellidos} -> {usuario(nombre, apellidos)}@alu.ulpgc.es")
Lánzalo con python3 limpiar.py:
1. Ana García Pérez -> ana.garcia@alu.ulpgc.es 2. Luis Romero Santana -> luis.romero@alu.ulpgc.es 3. María José Núñez de la Torre -> maria.nunez@alu.ulpgc.es 4. Marta Gil -> marta.gil@alu.ulpgc.es
Las cuatro líneas, tan distintas al entrar, salen con la misma forma. Fíjate en la tercera: "núñez de la torre , MARÍA JOSÉ" se ha convertido en María José Núñez de la Torre, con «de la» en minúscula, y su usuario es maria.nunez, sin tildes ni eñe. Vamos a ver cómo, función a función.
normalizar_espacios: split y join
def normalizar_espacios(texto):
"""Quita los espacios de los extremos y deja UN solo espacio entre palabras."""
return " ".join(texto.split()) # split() sin argumento corta por cualquier hueco
Es el truco de la teoría: texto.split() (sin argumento) corta por cualquier hueco, por grande que sea, y descarta los espacios de los extremos. Después, " ".join(…) vuelve a pegar las palabras con un solo espacio. Con "ROMERO SANTANA":
| Paso | Resultado |
|---|---|
texto | "ROMERO SANTANA" |
texto.split() | ['ROMERO', 'SANTANA'] |
" ".join(…) | "ROMERO SANTANA" |
¿Por qué no strip()? Porque strip() solo quita los espacios de los extremos; los tres espacios del medio seguirían ahí.
nombre_bonito: partition y title
def nombre_bonito(linea):
"""'gARCÍA pérez, ana' -> ('Ana', 'García Pérez')."""
apellidos, coma, nombre = linea.partition(",") # corta por la PRIMERA coma
nombre = normalizar_espacios(nombre).title()
apellidos = corregir_particulas(normalizar_espacios(apellidos).title())
return nombre, apellidos
Este es el recorrido completo de la tercera línea, la más complicada:
partition(",")corta por la primera coma y devuelve tres trozos: lo de antes ("núñez de la torre "), la coma y lo de después (" MARÍA JOSÉ"). La coma no la necesitamos, pero hay que recogerla en una variable (coma) para que el desempaquetado cuadre.- ¿Por qué
partitiony nosplit(",")? Porquepartitionsiempre devuelve exactamente tres trozos, aunque no haya coma (entonces los dos últimos son""). Consplit(","), una línea sin coma daría un solo trozo y el desempaquetado en dos variables fallaría. - Cada parte pasa por
normalizar_espaciosy portitle(). Como los strings son inmutables, cada llamada devuelve uno nuevo, y por eso se reasignanombre = …yapellidos = …. title()deja «Núñez De La Torre». En los apellidos españoles las partículas van en minúscula, así que los apellidos pasan además porcorregir_particulas.
Escribir normalizar_espacios(nombre).title() en una línea suelta, sin nombre = delante. No da error, pero nombre se queda como estaba: es exactamente el fallo del strip() del principio del tema.
corregir_particulas: cambiar una lista, no un string
def corregir_particulas(texto):
"""title() escribe 'De La'; las partículas van en minúscula (salvo si son la primera palabra)."""
palabras = texto.split()
for i in range(1, len(palabras)): # desde la 1: la primera palabra no se toca
if palabras[i].lower() in PARTICULAS:
palabras[i] = palabras[i].lower() # en una LISTA sí se puede cambiar un elemento
return " ".join(palabras)
Aquí está la gracia de la inmutabilidad. No podemos hacer texto[6] = "d" para cambiar la D de «De», porque un string no admite asignación por posición. Pero una lista sí. Así que partimos el texto en una lista de palabras, cambiamos las palabras que haga falta en la lista y la volvemos a juntar:
i | palabras[i] | .lower() | ¿Está en PARTICULAS? | palabras después |
|---|---|---|---|---|
| (inicio) | ['Núñez', 'De', 'La', 'Torre'] | |||
| 1 | 'De' | 'de' | Sí | ['Núñez', 'de', 'La', 'Torre'] |
| 2 | 'La' | 'la' | Sí | ['Núñez', 'de', 'la', 'Torre'] |
| 3 | 'Torre' | 'torre' | No | ['Núñez', 'de', 'la', 'Torre'] |
El bucle empieza en 1, no en 0, a propósito: si un apellido empezara por una partícula («De la Rosa»), la primera palabra debe seguir en mayúscula.
sin_tildes y usuario
def sin_tildes(texto):
"""Cambia cada vocal con tilde (y la ñ) por su versión sin tilde."""
for con, sin in TILDES.items():
texto = texto.replace(con, sin) # replace devuelve un string nuevo: hay que guardarlo
return texto
def usuario(nombre, apellidos):
"""Primera palabra del nombre + '.' + primer apellido, en minúsculas y sin tildes."""
primero = nombre.split()[0]
apellido1 = apellidos.split()[0]
return sin_tildes((primero + "." + apellido1).lower()) # primero lower: TILDES está en minúsculas
usuario toma la primera palabra del nombre ("María José".split()[0] es "María") y la primera del apellido ("Núñez"), los une con un punto y los pasa a minúsculas: "maría.núñez". El orden importa: primero lower() y después sin_tildes, porque el diccionario TILDES solo tiene las letras minúsculas. Si lo hiciéramos al revés, una Á mayúscula no se cambiaría.
sin_tildes recorre el diccionario y, para cada pareja, sustituye todas las apariciones. Con "maría.núñez":
| Vuelta | con → sin | texto después |
|---|---|---|
| 1 | á → a | maría.núñez (no hay á) |
| 2 | é → e | maría.núñez |
| 3 | í → i | maria.núñez |
| 4 | ó → o | maria.núñez |
| 5 | ú → u | maria.nuñez |
| 6 | ü → u | maria.nuñez |
| 7 | ñ → n | maria.nunez |
Cuando ya domines replace, Python tiene una forma de hacer todos los cambios de una vez: texto.translate(str.maketrans("áéíóúüñ", "aeiouun")). Hace lo mismo que el bucle, pero merece la pena escribir el bucle al menos una vez para entender qué pasa.
Pruébalo tú
- Añade a
CRUDAla línea"de la rosa, PABLO ". Predice el nombre y el usuario antes de ejecutar. ¿La primera palabra de los apellidos sale en mayúscula? ¿Y el usuario, tiene sentido que seapablo.de? - Escribe
iniciales(nombre, apellidos)que devuelva las iniciales en mayúscula y sin puntos (para María José Núñez de la Torre,MJNT: las partículas no cuentan).
Programa 2 · Validar DNIs y fechas de nacimiento
Cada alumno ha copiado a mano su DNI y su fecha de nacimiento, separados por ;. Queremos revisar cada registro y decir qué falla, no solo si está bien o mal: un DNI con una cifra de menos, una letra que no corresponde, una fecha sin el formato dd/mm/aaaa…
La idea
Un DNI español tiene 8 cifras y una letra, y la letra no es libre: es la que ocupa la posición número % 23 en el string "TRWAGMYFPDXBNJZSQVHLCKE". Así que la validación es una escalera de comprobaciones, de la más básica a la más fina: primero la longitud, luego que las 8 primeras sean cifras, y solo entonces calcular la letra. Si una falla, se devuelve el motivo y no se sigue. Con la fecha, lo mismo: formato (tres trozos de 2, 2 y 4 caracteres), que sean cifras y que el mes y el día tengan sentido.
El orden de las comprobaciones no es un capricho. Si calculas dni[8] antes de comprobar que el DNI mide 9, un DNI corto te dará un IndexError en lugar de un mensaje.
validar.py
LETRAS = "TRWAGMYFPDXBNJZSQVHLCKE" # la letra del DNI es LETRAS[número % 23]
# Cada registro: "DNI;fecha de nacimiento", copiado a mano por cada alumno
REGISTROS = [
"12345678Z;03/09/2007",
"1234567Z;15/02/2006",
"12345678A;21/11/2006",
"x2345678Z;30/06/2007",
"87654321x;7/5/2007",
" 45123456D ;31/13/2006 ",
]
def validar_dni(dni):
"""Devuelve "" si el DNI es correcto, o un texto con el motivo si no lo es."""
dni = dni.strip().upper() # tolera espacios y la letra en minúscula
if len(dni) != 9: # lo primero, la longitud: así dni[8] nunca falla
return f"DNI con {len(dni)} caracteres (deben ser 9)"
numero, letra = dni[:8], dni[8] # 8 cifras y la letra
if not numero.isdigit():
return f"'{numero}' no son 8 cifras"
esperada = LETRAS[int(numero) % 23] # el resto (de 0 a 22) es una posición de LETRAS
if letra != esperada:
return f"la letra debería ser {esperada}"
return ""
def validar_fecha(fecha):
"""Comprueba el formato dd/mm/aaaa y que el día y el mes tengan sentido."""
partes = fecha.strip().split("/")
if len(partes) != 3 or [len(p) for p in partes] != [2, 2, 4]:
return "fecha sin el formato dd/mm/aaaa"
if not "".join(partes).isdigit(): # juntar los trozos y mirar que todo sean cifras
return "fecha con caracteres que no son cifras"
dia, mes = int(partes[0]), int(partes[1])
if not 1 <= mes <= 12:
return f"el mes {mes} no existe"
if not 1 <= dia <= 31:
return f"el día {dia} no existe"
return ""
correctos = 0
print(f"{'DNI':<12}{'Fecha':<12}Resultado")
for registro in REGISTROS:
dni, fecha = registro.split(";")
# se guardan solo los mensajes no vacíos ("" cuenta como falso)
errores = [e for e in (validar_dni(dni), validar_fecha(fecha)) if e]
if errores:
resultado = "; ".join(errores)
else:
resultado = "OK"
correctos += 1
print(f"{dni.strip():<12}{fecha.strip():<12}{resultado}")
print(f"Correctos: {correctos} de {len(REGISTROS)} ({correctos / len(REGISTROS):.0%})")
Lánzalo con python3 validar.py:
DNI Fecha Resultado 12345678Z 03/09/2007 OK 1234567Z 15/02/2006 DNI con 8 caracteres (deben ser 9) 12345678A 21/11/2006 la letra debería ser Z x2345678Z 30/06/2007 'X2345678' no son 8 cifras 87654321x 7/5/2007 fecha sin el formato dd/mm/aaaa 45123456D 31/13/2006 el mes 13 no existe Correctos: 1 de 6 (17%)
validar_dni: una escalera de comprobaciones
def validar_dni(dni):
"""Devuelve "" si el DNI es correcto, o un texto con el motivo si no lo es."""
dni = dni.strip().upper() # tolera espacios y la letra en minúscula
if len(dni) != 9: # lo primero, la longitud: así dni[8] nunca falla
return f"DNI con {len(dni)} caracteres (deben ser 9)"
numero, letra = dni[:8], dni[8] # 8 cifras y la letra
if not numero.isdigit():
return f"'{numero}' no son 8 cifras"
esperada = LETRAS[int(numero) % 23] # el resto (de 0 a 22) es una posición de LETRAS
if letra != esperada:
return f"la letra debería ser {esperada}"
return ""
Cada if corta la función con un return en cuanto encuentra un problema; si se llega al final, se devuelve "", que significa «sin errores». Así se ve cada registro al pasar por la escalera:
| Registro | Tras strip().upper() | len | numero | ¿Cifras? | esperada | Resultado |
|---|---|---|---|---|---|---|
12345678Z | 12345678Z | 9 | 12345678 | Sí | Z | OK |
1234567Z | 1234567Z | 8 | — | — | — | DNI con 8 caracteres |
12345678A | 12345678A | 9 | 12345678 | Sí | Z | la letra debería ser Z |
x2345678Z | X2345678Z | 9 | X2345678 | No | — | no son 8 cifras |
87654321x | 87654321X | 9 | 87654321 | Sí | X | (DNI correcto) |
45123456D | 45123456D | 9 | 45123456 | Sí | D | (DNI correcto) |
La letra se calcula con un índice sobre un string, que es lo que mejor se entiende con un dibujo:
dni[:8]son las ocho cifras (como string) ydni[8], el noveno carácter.int(numero) % 23da el resto de dividir entre 23. Comprobémoslo con el primero: 23 × 536768 = 12345664, y 12345678 − 12345664 = 14. La posición 14 deLETRASes laZ: coincide.- El resto de dividir entre 23 siempre está entre 0 y 22, y
LETRAStiene 23 letras. Por esoLETRAS[resto]nunca se sale del string. strip().upper()al principio hace que la función tolere espacios y letras en minúscula: el quinto registro,87654321x, se da por bueno (87654321 % 23 = 10, yLETRAS[10]es laX). Por eso en el cuarto registro el mensaje dice'X2345678'con la equis mayúscula: el texto ya se había pasado a mayúsculas.
Comprobar las cifras con numero.isdigit() antes de recortar. Si haces dni.isdigit() con el DNI entero, la letra hace que salga siempre False. Primero el slice dni[:8], luego isdigit().
validar_fecha: partir y comprobar cada trozo
def validar_fecha(fecha):
"""Comprueba el formato dd/mm/aaaa y que el día y el mes tengan sentido."""
partes = fecha.strip().split("/")
if len(partes) != 3 or [len(p) for p in partes] != [2, 2, 4]:
return "fecha sin el formato dd/mm/aaaa"
if not "".join(partes).isdigit(): # juntar los trozos y mirar que todo sean cifras
return "fecha con caracteres que no son cifras"
dia, mes = int(partes[0]), int(partes[1])
if not 1 <= mes <= 12:
return f"el mes {mes} no existe"
if not 1 <= dia <= 31:
return f"el día {dia} no existe"
return ""
split("/")convierte"03/09/2007"en['03', '09', '2007']. La primera condición pide tres trozos y que midan 2, 2 y 4. La lista[len(p) for p in partes]da las longitudes; con"7/5/2007"sale[1, 1, 4], que no es[2, 2, 4], y la fecha se rechaza por el formato.- El orden de la condición importa: si la fecha no tuviera barras,
partestendría un solo trozo ylen(partes) != 3ya sería verdad. Elorno evalúa la segunda parte, así que nunca se intenta mirar trozos que no existen. "".join(partes)vuelve a juntar los trozos sin separador ("03092007") para comprobar de una vez que todo son cifras.- Solo cuando sabemos que son cifras se convierten con
int. Con"31/13/2006",mesvale 13 y la condición1 <= mes <= 12es falsa: el mes 13 no existe.
Esta validación es deliberadamente sencilla: acepta el 31 de febrero. Comprobar los días de cada mes (y los años bisiestos) es un buen «Pruébalo tú», y en el tema 8 verás cómo las expresiones regulares comprueban formatos como dd/mm/aaaa en una sola línea.
El bucle principal: juntar los errores
correctos = 0
print(f"{'DNI':<12}{'Fecha':<12}Resultado")
for registro in REGISTROS:
dni, fecha = registro.split(";")
# se guardan solo los mensajes no vacíos ("" cuenta como falso)
errores = [e for e in (validar_dni(dni), validar_fecha(fecha)) if e]
if errores:
resultado = "; ".join(errores)
else:
resultado = "OK"
correctos += 1
print(f"{dni.strip():<12}{fecha.strip():<12}{resultado}")
print(f"Correctos: {correctos} de {len(REGISTROS)} ({correctos / len(REGISTROS):.0%})")
registro.split(";")da exactamente dos trozos, DNI y fecha, que se desempaquetan.erroresse construye con una list comprehension que se queda solo con los mensajes no vacíos:if ees falso para"". Así, un registro con el DNI bien y la fecha mal tiene un solo error, y uno con los dos mal tendría dos, unidos por"; ".join(errores).- La salida usa
{dni.strip():<12}y{fecha.strip():<12}: columnas de 12 caracteres a la izquierda, que cuadran con la cabecera, escrita con los mismos anchos. - Al final, 1 correcto de 6 es 0,1666…, y el formato
:.0%lo multiplica por 100 y lo redondea sin decimales: 17%.
Pruébalo tú
- Mejora
validar_fechapara que compruebe los días de cada mes (con una lista[31, 28, 31, …]) y rechace el 30 de febrero. - Haz que
validar_dniacepte también NIE de extranjeros, que empiezan porX,YoZ: se sustituye esa letra por 0, 1 o 2 y se calcula igual. Prueba conX1234567L.
Programa 3 · El acta de los parciales, con columnas alineadas
Para terminar, el programa del gancho: las notas de los tres parciales llegan como strings con coma decimal ("7,5; 8,25; 6") en un diccionario alumno → notas. Queremos un acta como las de verdad: un título centrado, una cabecera, una fila por alumno con su número de lista, sus tres notas, la media y la calificación, todo alineado, y un resumen al final.
La idea
Hay dos problemas distintos y conviene separarlos. El primero es convertir cada string de notas en una lista de números (cortar por ;, limpiar, cambiar la coma por un punto, float). El segundo es presentar: decidir el ancho de cada columna y usarlo igual en la cabecera, en las filas y en el resumen. Si los anchos no coinciden, la tabla se descoloca.
Dibuja la tabla en papel antes de programar y cuenta los caracteres de cada columna. El ancho del nombre lo marca el más largo («Núñez de la Torre, María José» mide 29), y conviene dejar algún espacio de margen.
acta.py
# Notas de los tres parciales, tal como se copiaron de la hoja de cálculo (con coma decimal)
NOTAS = {
"Romero Santana, Luis": "5; 4,25; 6,5",
"García Pérez, Ana": "7,5; 8,25; 6",
"Núñez de la Torre, María José": "10; 9,5; 9,75",
"Gil, Marta": "4,5; 5; 3,75",
}
ANCHO = 75 # 4 + 31 + 6 + 6 + 6 + 7 + 2 = 62 columnas, más 13 de "Sobresaliente"
def convertir(texto):
"""'7,5; 8,25; 6' -> [7.5, 8.25, 6.0]"""
return [float(t.strip().replace(",", ".")) for t in texto.split(";")]
def calificacion(media):
if media < 5:
return "Suspenso"
if media < 7:
return "Aprobado"
if media < 9:
return "Notable"
return "Sobresaliente"
print(" ACTA DE FP1 · SUBGRUPO 43 ".center(ANCHO, "=")) # título centrado entre signos =
print(f"{'N.º':<4}{'Alumno':<31}{'P1':>6}{'P2':>6}{'P3':>6}{'Media':>7} Calificación")
print("-" * ANCHO)
medias = []
for n, alumno in enumerate(sorted(NOTAS), start=1): # sorted: por apellidos
p1, p2, p3 = convertir(NOTAS[alumno])
media = (p1 + p2 + p3) / 3
medias.append(media)
# zfill(2) para el número; cada nota a la derecha con 2 decimales
print(f"{str(n).zfill(2):<4}{alumno:<31}{p1:6.2f}{p2:6.2f}{p3:6.2f}{media:7.2f} {calificacion(media)}")
print("-" * ANCHO)
aprobados = sum(1 for m in medias if m >= 5)
print(f"{'Media de la clase:':<53}{sum(medias) / len(medias):7.2f}")
print(f"Aprobados: {aprobados} de {len(medias)} ({aprobados / len(medias):.0%})")
Lánzalo con python3 acta.py:
======================== ACTA DE FP1 · SUBGRUPO 43 ======================== N.º Alumno P1 P2 P3 Media Calificación --------------------------------------------------------------------------- 01 García Pérez, Ana 7.50 8.25 6.00 7.25 Notable 02 Gil, Marta 4.50 5.00 3.75 4.42 Suspenso 03 Núñez de la Torre, María José 10.00 9.50 9.75 9.75 Sobresaliente 04 Romero Santana, Luis 5.00 4.25 6.50 5.25 Aprobado --------------------------------------------------------------------------- Media de la clase: 6.67 Aprobados: 3 de 4 (75%)
convertir: de «7,5; 8,25; 6» a [7.5, 8.25, 6.0]
def convertir(texto):
"""'7,5; 8,25; 6' -> [7.5, 8.25, 6.0]"""
return [float(t.strip().replace(",", ".")) for t in texto.split(";")]
Todo ocurre en una list comprehension. Léela por partes, de dentro afuera, con las notas de Ana:
| Paso | Resultado |
|---|---|
texto.split(";") | ['7,5', ' 8,25', ' 6'] |
t.strip() (cada trozo) | '7,5', '8,25', '6' |
.replace(",", ".") | '7.5', '8.25', '6' |
float(…) | 7.5, 8.25, 6.0 |
Fíjate en que strip hace falta: split(";") deja el espacio que había después de cada ; pegado al trozo siguiente. En realidad float(" 8.25") toleraría ese espacio, pero limpiar antes es una buena costumbre: otras conversiones (y otras comparaciones) no son tan permisivas.
Con las tres notas, la media de Ana es (7.5 + 8.25 + 6) / 3 = 21.75 / 3 = 7.25, un Notable.
Las columnas, una a una
print(" ACTA DE FP1 · SUBGRUPO 43 ".center(ANCHO, "=")) # título centrado entre signos =
print(f"{'N.º':<4}{'Alumno':<31}{'P1':>6}{'P2':>6}{'P3':>6}{'Media':>7} Calificación")
print("-" * ANCHO)
medias = []
for n, alumno in enumerate(sorted(NOTAS), start=1): # sorted: por apellidos
p1, p2, p3 = convertir(NOTAS[alumno])
media = (p1 + p2 + p3) / 3
medias.append(media)
# zfill(2) para el número; cada nota a la derecha con 2 decimales
print(f"{str(n).zfill(2):<4}{alumno:<31}{p1:6.2f}{p2:6.2f}{p3:6.2f}{media:7.2f} {calificacion(media)}")
Este dibujo muestra la fila de Ana sobre una regla de caracteres. Cada campo de la f-string ocupa siempre el mismo ancho:
{str(n).zfill(2):<4}: el número de lista con dos cifras (01) en un campo de 4 a la izquierda.zfilles de los strings, por eso primero se conviertenconstr(con una f-string también valdría{n:02d}).{alumno:<31}: el nombre a la izquierda en 31 caracteres. El más largo mide 29, así que siempre quedan al menos 2 espacios antes de las notas.{p1:6.2f}: cada nota con 2 decimales en 6 caracteres. Al ser un número, va a la derecha sin tener que escribir>. Así10.00(5 caracteres) y7.50(4) terminan en la misma columna.{media:7.2f}: la media en 7, uno más que las notas, para que quede un poco separada.- La cabecera usa los mismos anchos:
{'Alumno':<31},{'P1':>6}… Aquí sí hay que escribir>, porque'P1'es un texto y se alinearía a la izquierda. ANCHOes 75: 4 + 31 + 6 + 6 + 6 + 7 = 60 caracteres de columnas, más 2 espacios, más los 13 de «Sobresaliente», la calificación más larga.- El título se centra con
center(ANCHO, "="): » ACTA DE FP1 · SUBGRUPO 43 » mide 27, y los 75 − 27 = 48 signos=se reparten, 24 a cada lado. sorted(NOTAS)devuelve las claves ordenadas. Como las claves empiezan por los apellidos, el acta sale en orden alfabético de apellidos, aunque el diccionario estuviera desordenado.
El resumen
print("-" * ANCHO)
aprobados = sum(1 for m in medias if m >= 5)
print(f"{'Media de la clase:':<53}{sum(medias) / len(medias):7.2f}")
print(f"Aprobados: {aprobados} de {len(medias)} ({aprobados / len(medias):.0%})")
{'Media de la clase:':<53} rellena el texto hasta 53 caracteres para que el número, con su ancho de 7, termine en la columna 53 + 7 = 60, la misma que la media de cada alumno. Comprueba la cuenta: (7.25 + 4.4166… + 9.75 + 5.25) / 4 = 26.6666… / 4 = 6.6666…, que con dos decimales es 6.67. Y 3 aprobados de 4 es el 75%.
Calcular la media con las notas todavía como strings. "7,5" + "8,25" no da error: da "7,58,25", porque + concatena. El error solo aparece después, al dividir entre 3. Convierte siempre a número antes de operar.
Cuando una tabla no cuadra, cuenta los anchos como en la regla del dibujo. La suma de los anchos de las columnas tiene que ser el ancho de la línea de guiones.
Pruébalo tú
- Añade un alumno con un nombre más largo de 31 caracteres. ¿Qué pasa con su fila? ¿Se recorta el nombre o se descoloca la tabla? Arréglalo recortando el nombre con un slice (
alumno[:30]). - Añade una columna con la nota máxima de cada alumno (
max(p1, p2, p3)) entre la media y la calificación, y ajusta la cabecera yANCHO.
Errores típicos en esta práctica
- Llamar a un método sin guardar el resultado.
linea.strip()suelto no cambialinea. Si el método devuelve un string, escribelinea = linea.strip(). - **Partir por
", "en lugar de por",".** En cuanto alguien escriba la coma sin espacio, no se parte. Parte por el separador mínimo y limpia los espacios después. - **Mirar
dni[8]antes de comprobar la longitud.** Un DNI corto daIndexError. Primerolen, luego índices. - **Usar
isdigit()para decimales.**"7,5".isdigit()y"7.5".isdigit()danFalse. Para convertir decimales,replaceyfloat. - Operar con notas que siguen siendo strings.
+concatena en lugar de sumar, y el error aparece más tarde y más lejos. - Anchos distintos en la cabecera y en las filas. La tabla se descoloca: escribe los mismos números en las dos f-strings.
Míralo paso a paso en Python Tutor
Python Tutor ejecuta el código línea a línea y muestra el valor de cada variable después de cada paso. Elige Python 3 y pega esta versión mínima de la limpieza de una línea:
linea = " gil, MARTA "
apellidos, coma, nombre = linea.partition(",")
apellidos = apellidos.strip().title()
nombre = " ".join(nombre.split()).title()
usuario = (nombre + "." + apellidos).lower()
print(nombre, apellidos, usuario)
Salida esperada:
Marta Gil marta.gil
Qué observar, paso a paso:
- Tras la línea 2 aparecen tres variables nuevas:
apellidosvale" gil",comavale","ynombrevale" MARTA ". Fíjate en los espacios:partitioncorta, pero no limpia. - En la línea 3,
apellidoscambia de valor a"Gil". En realidad no ha cambiado el string: Python ha creado uno nuevo y la variable apunta ahora a él. - En la línea 4,
nombrepasa de" MARTA "a"Marta":split()se come los espacios,joinpega la única palabra ytitle()arregla las mayúsculas. - En la línea 5,
usuariose construye sumando tres strings y pasando el resultado a minúsculas:"marta.gil". - Vuelve a la línea 2 con el botón Prev y observa que
lineanunca cambia durante todo el programa: todas las transformaciones crean strings nuevos.
Resumen
- Limpiar antes de usar:
strip," ".join(texto.split())ytitle()convierten texto escrito de cualquier manera en datos uniformes. - **
partitionfrente asplit:**partitionsiempre da tres trozos, así que se puede desempaquetar sin miedo;splitda tantos como cortes haya. - Para cambiar palabras sueltas, pasa a lista: los strings no admiten asignación por posición, pero una lista de palabras sí, y luego
joinla vuelve a juntar. - Validar es una escalera: longitud, después tipo de caracteres, después la regla fina (la letra, el mes). El orden evita errores como
IndexError. - Los índices sobre strings también sirven para buscar:
LETRAS[numero % 23]convierte un número en una letra. - Columnas alineadas = mismos anchos en cabecera, filas y resumen:
<para textos, números a la derecha con.2f, ycenter/zfillpara los detalles.
Checklist: ¿lo has practicado?
- ¿Has ejecutado los tres programas y comprobado las salidas?
- ¿Sabrías explicar por qué
nombre_bonitousapartition(",")y nosplit(", ")? - ¿Puedes calcular a mano la letra de un DNI con
% 23y la tablaLETRAS? - ¿Sabes por qué
validar_dnicomprueba la longitud antes que nada? - ¿Podrías añadir una columna al acta sin que se descoloque?
- ¿Has intentado los «Pruébalo tú» sin mirar la teoría?
Si has respondido que sí a casi todo, estás listo para los ejercicios de strings, donde ya no te doy el código hecho: correos de la universidad, firmas e iniciales, un parte de asistencia y tres fragmentos con fallos muy típicos para cazar. ¡Nos vemos allí!
Siguiente paso: ejerciciosStrings en Python — ejercicios con correos de la universidad, firmas con iniciales y un parte de asistencia (y los dos espacios que rompieron mis iniciales)¿Te ha servido este artículo? Así puedes apoyar SergioLearns →
