Python II: funciones y colecciones

Tema 2 · Fase 1, Programar con datos · Toda la ruta

En esta ficha · 5 secciones
  1. Antes de empezar
  2. Ejercicios
  3. Mini-proyecto: el reporte de notas de un curso
  4. Errores comunes
  5. ¿Lo lograste?

Antes de empezar

Varios ejercicios usan funciones y variables de la teoría: promedio, limpiar, contar, la_cuenta, leer_columna, palabras, conteo, la clase Contador y el archivo notas.csv. Abre un cuaderno nuevo en Colab, pega y ejecuta en orden los bloques de la ficha Código y después trabaja debajo. Intenta cada ejercicio antes de abrir la respuesta: equivocarse primero y corregir después es como se aprende de verdad.

Ejercicios

1. Predecir antes de ejecutar

Sin ejecutarlo, escribe en un papel qué imprime este programa. Después compruébalo.

def area(base, altura=2):
    return base * altura

def mostrar_area(base):
    print("El área es", area(base))

x = area(3)
y = area(3, 5)
z = mostrar_area(4)
print(x, y, z)
Respuesta
El área es 8
6 15 None
  • area(3) usa la altura por defecto, 2: da 6. area(3, 5) da 15.
  • mostrar_area(4) imprime «El área es 8» en el momento en que se llama, por eso esa línea sale primero. Como no tiene return, devuelve None, y eso es lo que queda guardado en z.

2. Una función para normalizar texto

Escribe una función normalizar(texto) que devuelva el texto en minúsculas, sin tildes (á → a, é → e, í → i, ó → o, ú → u; deja la ñ) y con un solo espacio entre palabras, sin espacios al principio ni al final. Pruébala con " ¡Qué RICO el Pan! ", que tiene que dar "¡que rico el pan!".

Pista: un diccionario sirve para decir qué letra cambia por cuál, y " ".join(lista) une una lista de palabras con un espacio entre cada una.

Respuesta
def normalizar(texto):
    """Minúsculas, sin tildes y con un solo espacio entre palabras."""
    sin_tilde = {"á": "a", "é": "e", "í": "i", "ó": "o", "ú": "u"}
    resultado = ""
    for c in texto.lower():
        resultado += sin_tilde.get(c, c)
    # split() sin argumentos corta en cualquier cantidad de espacios
    return " ".join(resultado.split())

print(repr(normalizar("  ¡Qué   RICO el  Pan! ")))
print(repr(normalizar("Canción   de   CUNA")))
'¡que rico el pan!'
'cancion de cuna'
  • sin_tilde.get(c, c) busca la letra en el diccionario; si no está, devuelve la misma letra. Así, la ñ y todo lo demás quedan igual.
  • repr muestra el texto con comillas, para que se vea que no quedaron espacios en los bordes.

3. La mediana a mano

La mediana de una lista de números es el valor del medio cuando se ordenan. Si la cantidad es par, es el promedio de los dos del medio. Escribe mediana(valores) sin usar ningún módulo y pruébala con [7, 1, 5] (da 5) y con [8, 3, 1, 10] (da 5,5). Después calcula la mediana de las notas de matemática de notas.csv y compárala con su promedio.

Respuesta
def mediana(valores):
    """El valor del medio de una lista de números."""
    ordenados = sorted(valores)
    n = len(ordenados)
    medio = n // 2
    if n % 2 == 1:
        return ordenados[medio]
    return (ordenados[medio - 1] + ordenados[medio]) / 2

print(mediana([7, 1, 5]), mediana([8, 3, 1, 10]))
matematica = leer_columna("notas.csv", "matematica")
print("Mediana:", mediana(matematica),
      "  Promedio:", round(promedio(matematica), 1))
5 5.5
Mediana: 72.5   Promedio: 69.5
  • Con 4 valores, medio es 2 y los del medio están en las posiciones 1 y 2 (recuerda que se cuenta desde 0).
  • Se usa sorted, que devuelve una lista nueva, y no valores.sort(), que ordenaría la lista original de quien llamó la función (mira el ejercicio 5).
  • La mediana (72,5) queda arriba del promedio (69,5) porque el 40 de Diego tira el promedio hacia abajo, pero a la mediana casi no la mueve.

4. Transformar listas con comprensiones

Tienes las temperaturas máximas de una semana, en grados Celsius:

semana = [18.5, 21.0, 23.4, 19.8, 25.1, 22.7, 17.9]

Con una comprensión de lista en cada caso (y la función celsius_a_fahrenheit de la teoría):

a) Crea la lista de las temperaturas en Fahrenheit, redondeadas a un decimal.

b) Crea la lista de las temperaturas mayores a 22 °C.

c) Crea la lista de los números de día (empezando en 1) en que la temperatura pasó de 22 °C. Pista: enumerate(semana, start=1) da pares (día, temperatura).

Respuesta
fahrenheit = [round(celsius_a_fahrenheit(t), 1) for t in semana]
calurosos = [t for t in semana if t > 22]
dias = [d for d, t in enumerate(semana, start=1) if t > 22]
print(fahrenheit)
print(calurosos)
print(dias)
[65.3, 69.8, 74.1, 67.6, 77.2, 72.9, 64.2]
[23.4, 25.1, 22.7]
[3, 5, 6]

En c), cada elemento de enumerate es una tupla (día, temperatura) que se desempaqueta en d, t; se guarda d solo si t pasa de 22.

5. La función que cambió mi lista

Este programa quiere sumar 5 puntos de bono a una copia de las notas y dejar las originales intactas. Ejecútalo. ¿Qué pasó con originales? ¿Por qué?

def con_bono(lista):
    for i in range(len(lista)):
        lista[i] = lista[i] + 5
    return lista

originales = [70, 45, 88]
nuevas = con_bono(originales)
print("Nuevas:    ", nuevas)
print("Originales:", originales)
Nuevas:     [75, 50, 93]
Originales: [75, 50, 93]

Y este otro, que quiere ordenar las notas, ¿por qué imprime None?

notas_curso = [70, 45, 88]
ordenadas = notas_curso.sort()
print(ordenadas)
None

Corrige los dos programas.

Respuesta

El primero. Al llamar con_bono(originales), el parámetro lista no recibe una copia: recibe la misma lista, con otro nombre. Cambiar lista[i] adentro de la función cambia la lista de afuera. nuevas y originales son dos nombres para una sola lista. La solución es crear una lista nueva, por ejemplo con una comprensión:

def con_bono(lista):
    return [n + 5 for n in lista]

originales = [70, 45, 88]
nuevas = con_bono(originales)
print("Nuevas:    ", nuevas)
print("Originales:", originales)
Nuevas:     [75, 50, 93]
Originales: [70, 45, 88]

Otra forma es copiar al principio de la función con lista = lista.copy().

El segundo. El método sort ordena la lista en su lugar (la cambia) y devuelve None. Si quieres una lista nueva ordenada, usa sorted; si te da igual cambiar la original, llama sort sin asignar:

notas_curso = [70, 45, 88]
ordenadas = sorted(notas_curso)
print(ordenadas, notas_curso)
notas_curso.sort()
print(notas_curso)
[45, 70, 88] [70, 45, 88]
[45, 70, 88]

Regla práctica: una función que recibe una lista no debería cambiarla, salvo que ese sea justamente su trabajo y lo diga su nombre.

6. Contar votos con un diccionario

En un curso votaron por la fruta para la merienda:

votos = ["mango", "plátano", "mango", "uva", "piña", "mango",
         "uva", "plátano", "mango", "uva", "piña", "uva", "mango"]

a) Usa la función contar de la teoría para obtener cuántos votos tuvo cada fruta.

b) Imprime las frutas de la más votada a la menos votada, una por línea, con su cantidad y su porcentaje.

c) ¿Cuántas letras distintas se usaron en total en los votos? Usa un conjunto.

Respuesta
resultado = contar(votos)
print(resultado)

total = len(votos)
for fruta, n in sorted(resultado.items(), key=la_cuenta, reverse=True):
    print(f"{fruta:<8} {n} votos  ({n / total:.0%})")

letras = set()
for fruta in votos:
    for letra in fruta:
        letras.add(letra)
print(len(letras), "letras distintas:", "".join(sorted(letras)))
{'mango': 5, 'plátano': 2, 'uva': 4, 'piña': 2}
mango    5 votos  (38%)
uva      4 votos  (31%)
plátano  2 votos  (15%)
piña     2 votos  (15%)
13 letras distintas: agilmnoptuváñ
  • {fruta:<8} rellena el nombre con espacios hasta 8 caracteres, para que las columnas queden alineadas; {n / total:.0%} escribe una proporción como porcentaje sin decimales.
  • Para la «á» de plátano, el conjunto la cuenta como una letra distinta de la «a». Si quisieras contarlas juntas, primero pasarías cada voto por normalizar del ejercicio 2.

7. Palabras en común

Con limpiar y conjuntos, encuentra las palabras que aparecen en las dos frases y las que aparecen solo en la primera. Los operadores de conjuntos son a & b (las que están en los dos) y a - b (las que están en a pero no en b).

frase_1 = "El perro de Ana corre en el parque."
frase_2 = "Ana corre todas las mañanas en el parque del barrio."
Respuesta
a = set(limpiar(frase_1).split())
b = set(limpiar(frase_2).split())
print("En las dos:      ", sorted(a & b))
print("Solo en la 1.ª:  ", sorted(a - b))
En las dos:       ['ana', 'corre', 'el', 'en', 'parque']
Solo en la 1.ª:   ['de', 'perro']

Sin limpiar, «parque.» (con punto) y «parque» serían palabras distintas y no aparecerían en común; «El» y «el», tampoco.

8. Las ventas de una tienda

Este código crea un CSV de ejemplo con las ventas de una semana en una tienda pequeña:

ventas = [
    ["dia", "producto", "cantidad"],
    ["lunes", "pan", 40], ["lunes", "leche", 12], ["lunes", "queso", 3],
    ["martes", "pan", 35], ["martes", "leche", 15],
    ["miercoles", "pan", 42], ["miercoles", "queso", 5],
    ["jueves", "pan", 38], ["jueves", "leche", 10], ["jueves", "queso", 4],
    ["viernes", "pan", 50], ["viernes", "leche", 18],
]
with open("ventas.csv", "w", newline="", encoding="utf-8") as f:
    csv.writer(f).writerows(ventas)

a) Léelo solo con open (sin el módulo csv) y cuenta cuántas filas de datos tiene, sin contar la de los nombres de las columnas.

b) Léelo con csv.DictReader y calcula, con un diccionario, el total vendido de cada producto.

Respuesta
# a) Solo con open: la primera línea son los nombres
with open("ventas.csv", encoding="utf-8") as f:
    lineas = f.readlines()
print(lineas[0].strip())
print(len(lineas) - 1, "filas de datos")

# b) Con csv.DictReader y un diccionario producto → total
totales = {}
with open("ventas.csv", encoding="utf-8") as f:
    for fila in csv.DictReader(f):
        producto = fila["producto"]
        cantidad = int(fila["cantidad"])
        totales[producto] = totales.get(producto, 0) + cantidad
print(totales)
dia,producto,cantidad
12 filas de datos
{'pan': 205, 'leche': 55, 'queso': 12}
  • f.readlines() lee todo el archivo de una vez y devuelve una lista con una línea por elemento.
  • La línea clave de b) es la misma que la de contar palabras, pero sumando cantidad en lugar de 1. Un diccionario que acumula por clave es uno de los patrones más útiles de todo el análisis de datos.
  • Sin int(...), totales.get(producto, 0) + cantidad fallaría con un TypeError, porque cantidad llegaría como texto.

9. Instalar un paquete

a) En Colab quieres usar el paquete wordcloud (dibuja «nubes de palabras») y al escribir import wordcloud aparece un error. ¿Qué error esperas ver y qué escribes en una celda para arreglarlo?

b) Quieres instalar el paquete que trae LinearRegression. Su nombre para instalar es scikit-learn. ¿Con qué nombre lo importas?

c) ¿Cuál es la diferencia entre import random y pip install?

Respuesta

a) Un ModuleNotFoundError: No module named 'wordcloud'. Se arregla con una celda que diga:

!pip install wordcloud

Después de instalarlo, vuelve a ejecutar la celda del import.

b) Con import sklearn (o, más común, from sklearn.linear_model import LinearRegression). El nombre para instalar y el nombre para importar no siempre coinciden.

c) pip install descarga e instala un paquete en la computadora: se hace una vez. import carga en tu programa un módulo que ya está instalado: se hace en cada programa que lo use. random viene con Python, así que nunca hace falta instalarlo.

10. Mejorar la clase Contador

Agrega a la clase Contador de la teoría dos métodos:

  • total(), que devuelve cuántas cosas se agregaron en total;
  • proporcion(cosa), que devuelve qué fracción del total es esa cosa (0 si nunca se agregó).

Pruébala con las palabras del texto de la panadería: ¿qué proporción de las palabras es «pan»?

Respuesta
class Contador:
    """Cuenta cuántas veces aparece cada cosa."""

    def __init__(self):
        self.conteo = {}

    def agregar(self, cosa):
        self.conteo[cosa] = self.conteo.get(cosa, 0) + 1

    def agregar_todas(self, cosas):
        for cosa in cosas:
            self.agregar(cosa)

    def mas_comunes(self, k):
        pares = sorted(self.conteo.items(), key=lambda par: par[1],
                       reverse=True)
        return pares[:k]

    def total(self):
        return sum(self.conteo.values())

    def proporcion(self, cosa):
        return self.conteo.get(cosa, 0) / self.total()

c = Contador()
c.agregar_todas(palabras)
print(c.total(), "palabras")
print(f"pan: {c.proporcion('pan'):.1%}   queso: {c.proporcion('queso')}")
63 palabras
pan: 6.3%   queso: 0.0

self.conteo.values() da solo los valores del diccionario (las cuentas), y sum los suma. Los métodos nuevos usan self.conteo, el mismo dato que llenó agregar_todas: eso es lo que hace útil a un objeto.

Mini-proyecto: el reporte de notas de un curso

Vas a escribir un programa que lee las notas de un curso desde un CSV e imprime un reporte. Los datos son de ejemplo, inventados para este ejercicio: diez estudiantes con las notas de tres trimestres, sobre 100.

curso = [
    ["nombre", "trimestre_1", "trimestre_2", "trimestre_3"],
    ["Andrea", 72, 78, 85],
    ["Benjamín", 45, 52, 58],
    ["Camila", 88, 91, 87],
    ["Daniel", 60, 55, 49],
    ["Fernanda", 93, 95, 98],
    ["Gabriel", 50, 61, 70],
    ["Isabel", 77, 74, 80],
    ["Joaquín", 38, 47, 55],
    ["Lucía", 81, 79, 76],
    ["Mateo", 66, 70, 73],
]
with open("curso.csv", "w", newline="", encoding="utf-8") as f:
    csv.writer(f).writerows(curso)

Escribe tu programa en funciones, y que al final imprima:

  1. Cada estudiante con su promedio, ordenados del mejor al peor promedio.
  2. Cuántos estudiantes aprueban, si se aprueba con un promedio de 51 o más, y quiénes no.
  3. El promedio del curso en cada trimestre.
  4. Quién mejoró más del primer al tercer trimestre, y en cuántos puntos.

Una forma de organizarlo: una función leer_notas(archivo) que devuelva un diccionario nombre → lista de tres notas; las funciones promedio de la teoría y mediana del ejercicio 3 para los cálculos; y una función reporte(notas) que imprima todo.

Una solución
def leer_notas(archivo):
    """Diccionario nombre → lista de notas (números)."""
    notas = {}
    with open(archivo, encoding="utf-8") as f:
        for fila in csv.DictReader(f):
            nombre = fila["nombre"]
            notas[nombre] = [float(fila[f"trimestre_{t}"])
                             for t in (1, 2, 3)]
    return notas


def reporte(notas, minimo=51):
    """Imprime el reporte del curso."""
    promedios = {n: promedio(v) for n, v in notas.items()}

    print("1. Promedios, del mejor al peor")
    for nombre, p in sorted(promedios.items(), key=la_cuenta,
                            reverse=True):
        print(f"   {nombre:<10} {p:5.1f}")

    reprobados = [n for n, p in promedios.items() if p < minimo]
    print(f"2. Aprueban {len(notas) - len(reprobados)} de {len(notas)}."
          f" No aprueban: {', '.join(reprobados)}")

    print("3. Promedio del curso por trimestre")
    for t in range(3):
        del_trimestre = [v[t] for v in notas.values()]
        print(f"   Trimestre {t + 1}: {promedio(del_trimestre):.1f}"
              f"  (mediana {mediana(del_trimestre):.1f})")

    mejoras = {n: v[2] - v[0] for n, v in notas.items()}
    mejor = max(mejoras, key=mejoras.get)
    print(f"4. Mejoró más: {mejor}, {mejoras[mejor]:+.0f} puntos")


reporte(leer_notas("curso.csv"))
1. Promedios, del mejor al peor
   Fernanda    95.3
   Camila      88.7
   Lucía       78.7
   Andrea      78.3
   Isabel      77.0
   Mateo       69.7
   Gabriel     60.3
   Daniel      54.7
   Benjamín    51.7
   Joaquín     46.7
2. Aprueban 9 de 10. No aprueban: Joaquín
3. Promedio del curso por trimestre
   Trimestre 1: 67.0  (mediana 69.0)
   Trimestre 2: 70.2  (mediana 72.0)
   Trimestre 3: 73.1  (mediana 74.5)
4. Mejoró más: Gabriel, +20 puntos
  • leer_notas arma cada lista con una comprensión que lee las columnas trimestre_1, trimestre_2 y trimestre_3.
  • max(mejoras, key=mejoras.get) recorre las claves del diccionario y se queda con la que tiene el valor más grande: la misma idea de key que en sorted.
  • El promedio del curso sube trimestre a trimestre, pero no todos mejoraron: Daniel y Lucía bajaron. Un promedio general esconde a las personas; por eso conviene mirar también cada caso.
  • Para ir más allá: guarda el reporte en un archivo reporte.txt (abre con "w" y usa f.write(...) en lugar de print), o grafica los promedios con plt.barh, como las palabras de la teoría.

Errores comunes

  • Olvidar el return. Una función que solo hace print devuelve None; si guardas su resultado, guardas nada.
  • Usar fuera una variable que se creó dentro de una función. Es local y no existe afuera (NameError). Devuélvela con return.
  • Cambiar sin querer la lista que recibió una función. El parámetro es la misma lista, no una copia. Crea una lista nueva o usa .copy().
  • Escribir lista = lista.sort(). sort ordena en su lugar y devuelve None. Usa sorted(lista) si quieres una lista nueva.
  • Contar desde 1. El primer elemento es lista[0] y el último es lista[-1] o lista[len(lista) - 1]. lista[len(lista)] da IndexError.
  • Buscar una clave que no está. d["clave"] da KeyError si no existe; usa "clave" in d para preguntar o d.get("clave", 0) para tener un valor por defecto.
  • No limpiar el texto antes de contar. «Pan», «pan,» y «pan» son tres claves distintas para la computadora.
  • Hacer cuentas con lo que se leyó de un archivo sin convertirlo. De un CSV todo llega como texto: int(...) o float(...) antes de sumar.
  • Pasar key=la_cuenta() con paréntesis. key recibe la función, no su resultado: key=la_cuenta.
  • Escribir pip install dentro del código Python. En Colab va en su propia celda con ! adelante; en tu computadora, en la terminal.

¿Lo lograste?

Al terminar este tema deberías poder hacer lo siguiente. Al lado, dónde lo practicaste.

  • Escribir funciones con parámetros y return (ejercicios 1, 2 y 3, y el mini-proyecto).
  • Recorrer y transformar listas, también con comprensiones, sin cambiar sin querer la original (ejercicios 4 y 5).
  • Contar frecuencias con un diccionario y ordenarlas con sorted y key (ejercicios 6 y 7).
  • Leer un CSV con open y con el módulo csv (ejercicio 8 y el mini-proyecto).
  • Instalar un paquete con pip y cargarlo con import (ejercicio 9).
  • Reconocer una clase, un objeto y sus métodos (ejercicio 10).