Capítulo 5 de 14 10 secciones 9 min

Funciones: dejar de copiar y pegar

Empaquetar trabajo con nombre, devolver resultados, y la trampa del argumento por defecto que muerde en producción.

Una función empaqueta un trozo de trabajo bajo un nombre para poder usarlo muchas veces sin repetir el código. Se define con def, recibe argumentos y devuelve un resultado con return. La trampa que más sorprende es usar una lista o un diccionario como valor por defecto: ese valor se crea una sola vez y queda compartido entre todas las llamadas.

Hola! El momento de dejar de copiar y pegar

Si llegaste hasta aquí ya escribiste el mismo bloque de código dos o tres veces cambiando un número. A todas nos pasa 😅

Una función es trabajo con nombre. Lo escribes una vez, le pones un nombre, y lo usas todas las veces que quieras.

def con_igv(monto):
    return round(monto * 1.18, 2)

print(con_igv(100))
print(con_igv(480.37))
118.0
566.84

Tres piezas y ya está: def para definirla, el nombre y los paréntesis con lo que recibe, y return para devolver el resultado.

Y sí, la sangría manda igual que en el capítulo 4: lo que está dentro de la función lleva sus cuatro espacios.

return no es print

Esta confusión la tiene todo el mundo al principio, así que veámosla de frente.

def suma_mala(a, b):
    print(a + b)

def suma_buena(a, b):
    return a + b

resultado_malo = suma_mala(2, 3)
resultado_bueno = suma_buena(2, 3)

print('malo :', resultado_malo)
print('bueno:', resultado_bueno)
5
malo : None
bueno: 5

Las dos "muestran" un 5, pero solo una devuelve algo con lo que puedas seguir trabajando. La primera imprimió y se olvidó, así que resultado_malo quedó en None.

La regla: una función calcula y devuelve; quien la llama decide si imprime. Si la función imprime por su cuenta, no la puedes reutilizar dentro de otro cálculo.

Argumentos: por posición y por nombre

def total_venta(unidades, precio, descuento):
    bruto = unidades * precio
    return round(bruto * (1 - descuento), 2)

print(total_venta(10, 45.0, 0.15))
print(total_venta(unidades=10, precio=45.0, descuento=0.15))
print(total_venta(10, descuento=0.15, precio=45.0))
382.5
382.5
382.5

Las tres llamadas hacen lo mismo. Pero mira la primera y dime de memoria qué significa ese 0,15 sin subir a leer la definición 🤔

Por eso, cuando una función recibe más de dos cosas, llámala por nombre. Tu yo del futuro te lo va a agradecer.

Y el orden importa cuando mezclas: los posicionales van primero, siempre.

total_venta(unidades=10, 45.0, 0.15)
SyntaxError: positional argument follows keyword argument

Valores por defecto

def total_venta(unidades, precio, descuento=0, igv=0.18):
    bruto = unidades * precio * (1 - descuento)
    return round(bruto * (1 + igv), 2)

print(total_venta(10, 45.0))
print(total_venta(10, 45.0, descuento=0.15))
print(total_venta(10, 45.0, igv=0))
531.0
451.35
450.0

Lo que casi siempre vale igual se pone por defecto, y quien necesite otra cosa lo dice. Así la llamada normal queda cortita y la excepcional sigue siendo posible.

La trampa del argumento por defecto

Esta te la enseño ahora porque es de las que muerden en producción y casi nadie la explica en español. Presta atención un minuto.

def agrega_venta(monto, historial=[]):
    historial.append(monto)
    return historial

print(agrega_venta(100))
print(agrega_venta(200))
print(agrega_venta(300))
[100]
[100, 200]
[100, 200, 300]

¿Ves lo que pasó? Cada llamada debería empezar con una lista vacía, y en vez de eso se van acumulando 😳

La razón: el valor por defecto se crea una sola vez, cuando se define la función, no en cada llamada. Así que las tres llamadas están usando la misma lista, exactamente como el aliasing del capítulo 3.

La solución es siempre la misma y se escribe así:

def agrega_venta(monto, historial=None):
    if historial is None:
        historial = []
    historial.append(monto)
    return historial

print(agrega_venta(100))
print(agrega_venta(200))
[100]
[200]

Regla dura: nunca pongas una lista, un diccionario ni un conjunto como valor por defecto. Usa None y créalo dentro. Con números, textos y True o False no hay problema, porque esos no se pueden modificar.

Devolver varias cosas

def resumen(montos):
    return len(montos), sum(montos), sum(montos) / len(montos)

cuantas, total, promedio = resumen([480.37, 154.83, 2480.84])

print(cuantas)
print(f'{total:,.2f}')
print(f'{promedio:,.2f}')
3
3,116.04
1,038.68

Por dentro devuelve una tupla, como vimos en el capítulo 3, y al recibirla la desempaquetas en tres variables. Es limpísimo.

Cuando son más de tres cosas, mejor devuelve un diccionario: los nombres sobreviven y no dependes del orden.

def resumen(montos):
    return {
        'cuantas': len(montos),
        'total': sum(montos),
        'promedio': sum(montos) / len(montos),
        'maximo': max(montos),
    }

r = resumen([480.37, 154.83, 2480.84])
print(r['promedio'])
1038.68

Dónde vive cada variable

Lo que creas dentro de una función se muere cuando la función termina. Eso se llama alcance y es una protección, no un capricho.

def calcular():
    interno = 42
    return interno * 2

print(calcular())
print(interno)
NameError: name 'interno' is not defined

Al revés sí funciona: desde dentro se ve lo de fuera.

IGV = 0.18          # constante, en mayusculas por costumbre

def con_impuesto(monto):
    return monto * (1 + IGV)

print(con_impuesto(100))
118.0

Ahora, que se pueda no significa que se deba. Una función que depende de variables de fuera es una función que solo funciona en tu cuaderno. Para constantes de verdad como el IGV está bien; para datos, pásalos como argumento.

Docstrings: explicar para qué sirve

Un texto entre triples comillas justo debajo del def es la documentación de la función. No es adorno: help() la lee, tu editor la muestra, y en un equipo es la diferencia entre que usen tu función o la reescriban.

def clasifica_venta(monto):
    """
    Devuelve la categoria comercial de una venta segun su monto.

    Los cortes los fijo comercial en enero de 2026 y se revisan cada
    semestre, asi que si cambian, se cambian aqui y en un solo sitio.
    """
    if monto > 2000:
        return 'Mayorista'
    if monto > 500:
        return 'Mediana'
    return 'Pequeña'

print(clasifica_venta(780))
print(clasifica_venta.__doc__.strip().splitlines()[0])
Mediana
Devuelve la categoria comercial de una venta segun su monto.

Fíjate en algo de esa función: como cada rama hace return, no hacen falta los elif. Al hacer return se sale de la función ahí mismo. Queda más plano y se lee mejor 🌸

lambda: funciones de una línea

Una lambda es una función chiquita y sin nombre. Se usa para pasarla como argumento a otra cosa, y ahí es donde brilla.

ventas = [
    {'ciudad': 'Lima', 'monto': 480.37},
    {'ciudad': 'Cusco', 'monto': 2480.84},
    {'ciudad': 'Piura', 'monto': 154.83},
]

por_monto = sorted(ventas, key=lambda v: v['monto'], reverse=True)
print([v['ciudad'] for v in por_monto])
['Cusco', 'Lima', 'Piura']

Ese key= le dice a sorted qué mirar de cada elemento para comparar. Sin la lambda tendrías que definir una función entera para algo de una línea.

Mi consejo: usa lambda solo para eso, para pasarla como argumento. Si le vas a poner nombre, entonces era una función normal.

Una función de verdad, con todo junto

def limpia_ciudad(texto, por_defecto='Sin ciudad'):
    """Normaliza el nombre de una ciudad que viene de un sistema externo."""
    if texto is None:
        return por_defecto

    limpio = texto.strip().title()
    if limpio == '':
        return por_defecto

    equivalencias = {'Lma': 'Lima', 'Aqp': 'Arequipa', 'Cuzco': 'Cusco'}
    return equivalencias.get(limpio, limpio)

for entrada in ['  lima ', 'CUZCO', 'Aqp', '', None, 'Piura']:
    print(f'{repr(entrada):10} -> {limpia_ciudad(entrada)}')
'  lima '  -> Lima
'CUZCO'    -> Cusco
'Aqp'      -> Arequipa
''         -> Sin ciudad
None       -> Sin ciudad
'Piura'    -> Piura

Eso es una función lista para producción: tiene su docstring, maneja el nulo, maneja el vacío, tiene un valor por defecto que quien llama puede cambiar, y las equivalencias están en un solo sitio donde se pueden corregir.

Y lo mejor: la puedes probar con seis entradas en dos segundos, que es exactamente lo que acabamos de hacer 💜

Ejercicios

1. Tu primera función

Escribe una función que reciba un monto y devuelva el descuento del 12%, redondeado a dos decimales.

def descuento(monto):
    return round(monto * 0.12, 2)

print(descuento(480.37))
57.64
2. return contra print

Escribe la misma función con print en vez de return e intenta usar su resultado en otra cuenta.

def descuento_print(monto):
    print(round(monto * 0.12, 2))

r = descuento_print(480.37)
print(r)
57.64
None

El 57,64 lo viste, pero no lo tienes. Y None + algo revienta con un TypeError, que es como se descubre este error en la práctica.

3. Con valor por defecto

Haz que el porcentaje de descuento sea un argumento con 12% por defecto, y pruébalo con y sin él.

def descuento(monto, porcentaje=0.12):
    return round(monto * porcentaje, 2)

print(descuento(1000))
print(descuento(1000, porcentaje=0.25))
120.0
250.0
4. Tres valores de vuelta

Una función que reciba una lista de montos y devuelva el mínimo, el máximo y el rango.

def extremos(montos):
    return min(montos), max(montos), max(montos) - min(montos)

bajo, alto, rango = extremos([480.37, 154.83, 2480.84])
print(bajo, alto, round(rango, 2))
154.83 2480.84 2326.01
5. La trampa, provocada

Escribe la función con la lista por defecto, llámala tres veces y explica la salida. Después arréglala.

def mal(venta, carrito=[]):
    carrito.append(venta)
    return carrito

print(mal('Abarrotes'), mal('Bebidas'), mal('Snacks'))

def bien(venta, carrito=None):
    if carrito is None:
        carrito = []
    carrito.append(venta)
    return carrito

print(bien('Abarrotes'), bien('Bebidas'), bien('Snacks'))
['Abarrotes', 'Bebidas', 'Snacks'] ['Abarrotes', 'Bebidas', 'Snacks'] ['Abarrotes', 'Bebidas', 'Snacks']
['Abarrotes'] ['Bebidas'] ['Snacks']

En la primera línea salen tres veces la misma lista porque los tres return devuelven el mismo objeto, ya con todo dentro.

6. Validar antes de calcular

Una función que calcule el ticket promedio y devuelva 0 si la lista viene vacía, en vez de reventar.

def ticket_promedio(montos):
    if not montos:
        return 0
    return round(sum(montos) / len(montos), 2)

print(ticket_promedio([100, 200, 300]))
print(ticket_promedio([]))
200.0
0

Sin ese if te sale un ZeroDivisionError. Y las listas vacías llegan más seguido de lo que uno cree: un filtro que no encontró nada ya te dejó una.

7. El alcance

Comprueba que una variable creada dentro de una función no existe fuera.

def total_con_igv(monto):
    igv = monto * 0.18          # solo existe aqui dentro
    return monto + igv

print(total_con_igv(100))
print(igv)
NameError: name 'igv' is not defined
8. Ordenar con lambda

Ordena una lista de clientes por su nombre sin que las mayúsculas alteren el orden.

clientes = ['bodega rosa', 'Abarrotes Luz', 'MINIMARKET SOL']

print(sorted(clientes, key=lambda c: c.lower()))
['Abarrotes Luz', 'bodega rosa', 'MINIMARKET SOL']
9. Una función que usa otra

Escribe con_igv y con_descuento, y después una tercera que las combine.

def con_igv(monto, igv=0.18):
    return monto * (1 + igv)

def con_descuento(monto, dcto=0.10):
    return monto * (1 - dcto)

def precio_final(base):
    return round(con_igv(con_descuento(base)), 2)

print(precio_final(1000))
1062.0

Funciones chiquitas que se combinan es exactamente la idea. Y de paso: el orden importa, porque el IGV se calcula sobre el monto ya descontado y no al revés.

10. Con su docstring

Escribe una función que clasifique un puntaje de satisfacción del 1 al 5, con docstring, y muéstrala con help.

def clasifica(puntaje):
    """Traduce un puntaje de 1 a 5 a una etiqueta de riesgo."""
    if puntaje >= 4:
        return 'Satisfecho'
    if puntaje == 3:
        return 'Neutral'
    return 'En riesgo'

print(clasifica(5), clasifica(3), clasifica(1))
print(clasifica.__doc__)
Satisfecho Neutral En riesgo
Traduce un puntaje de 1 a 5 a una etiqueta de riesgo.

Lo que te llevas

  • 🎁 Una función calcula y return; quien la llama decide si imprime.
  • 🏷️ Con más de dos argumentos, llámalos por nombre.
  • 💣 Nunca una lista o un diccionario como valor por defecto: usa None.
  • 📦 Varias cosas de vuelta se desempaquetan; si son muchas, devuelve un diccionario.
  • 📝 El docstring dice para qué sirve y de dónde salieron los números.

En el capítulo 6 vamos a lo que pasa cuando algo falla: los errores más comunes con datos, cómo atraparlos sin taparlos, y cómo depurar sin volverte loca.

Que tengas un hermoso día! 🌟

¿Tienes alguna duda o consulta?