La idea
Una función es un trozo de lógica con nombre y una puerta a cada lado. Los valores entran por los parámetros. Un valor sale por return. En medio, la función trabaja en un espacio de trabajo privado.
Figura 1
Una llamada a line_total, de los argumentos al valor devuelto
-
line_total(92.99, 1, 10)la llamada -
price=92.99, qty=1, discount_pct=10los argumentos pasan a ser parámetros -
price * qty * (1 - discount_pct / 100)se ejecuta el cuerpo -
return round(..., 2) -> 83.69vuelve un solo valor
- La línea que llama a la función le entrega tres valores, en orden. La llamada se pausa mientras la función trabaja.
- Python crea un espacio de trabajo nuevo y privado y ata los nombres de los parámetros a los argumentos, emparejados por posición (o por nombre si escribes
discount_pct=10). - El cuerpo se ejecuta usando solo esos nombres. 92.99 × 1 × 0.9 es 83.691. Nada fuera de la función puede ver
priceniqty. returntermina la función y envía un valor de vuelta al lugar de la llamada. El espacio de trabajo se tira, y la llamada se sustituye por 83.69.
Cuando llamas a line_total(92.99, 1, 10), Python pausa tu código, crea ese espacio de trabajo, ata los nombres de los parámetros a los argumentos que pasaste, ejecuta el cuerpo y sustituye toda la llamada por el valor devuelto. Después tu código sigue como si hubieras escrito 83.69 donde estaba la llamada.
Por qué existe
Copiar un cálculo y cambiarle un número es como se multiplican los errores. La regla del descuento se pega en nueve sitios, alguien corrige ocho, y los totales del noveno quedan mal durante un mes. Una función le da a la regla un solo hogar con un solo nombre. La corriges ahí y todos quienes la llaman quedan corregidos.
También hace el código legible. line_total(price, qty, discount_pct) dice qué se calcula. La misma aritmética en línea no dice nada hasta que la descifras. En los capítulos de pandas pasarás funciones a otras funciones, como sorted(..., key=...) aquí y apply más adelante, y eso solo fluye si piensas en una función como un valor al que puedes ponerle nombre y entregar.
Cómo funciona por dentro
Parámetros, argumentos y valores por defecto
Un parámetro es el nombre en la definición. Un argumento es el valor en la llamada. Se emparejan por posición, o por nombre cuando escribes discount_pct=10. Un valor por defecto como discount_pct=0 se usa cuando quien llama omite el argumento, lo que acorta la llamada común: line_total(11.99, 4).
Los valores por defecto se evalúan una sola vez, cuando se define la función, así que nunca uses una lista o un diccionario como valor por defecto. Usa None y crea la lista dentro del cuerpo.
Return es la única salida
Figura 2
La misma función, aplicada a tres líneas de pedido
| price | qty | discount_pct |
|---|---|---|
| 92.99 | 1 | 10 |
| 349.00 | 1 | 10 |
| 11.99 | 4 | 0 |
| line_total |
|---|
| 83.69 |
| 314.10 |
| 47.96 |
- Tres líneas de pedido reales. La regla del total de una línea es la misma cada vez: precio × cantidad, menos el descuento.
- Escribe la regla una vez como
line_total(price, qty, discount_pct=0). El= 0hace opcional el descuento: quien no tiene descuento puede omitirlo. - Llámala una vez por línea: 83.69, 314.10 y 47.96 para la línea sin descuento. Una función, tres llamadas, una sola regla que corregir si la matemática del descuento cambia.
Lo que una función devuelve es lo que siga a return. De ahí salen dos errores. Una función con print y sin return muestra un valor pero devuelve None, así que la siguiente línea de código no recibe nada con lo que trabajar. Y el código después de un return nunca se ejecuta, porque la función ya terminó. Ese segundo punto es útil: el return anticipado te deja atender primero el caso especial (if qty == 0: return 0) y escribir la lógica principal sin sangría de más. Una función puede devolver varios valores devolviendo una tupla, return total, count, y quien llama los desempaqueta: total, count = summarise(cart).
Los nombres locales se quedan locales
Un nombre asignado dentro de una función existe solo durante esa llamada. Una función puede leer un nombre de fuera, pero asignarlo crea uno local nuevo. Por eso dos funciones pueden usar una variable llamada total sin interferir. También es la razón de pasar como argumentos lo que una función necesita, en vez de esperar que ande por ahí la global correcta.
La excepción: los objetos compartidos
Los nombres locales no protegen los objetos a los que apuntan. Si pasas una lista, el parámetro es un segundo nombre para la misma lista. Cambia la lista a través de él y quien llama ve el cambio.
El patrón seguro es el que también sigue pandas: una función crea un resultado nuevo y lo devuelve, [p * 0.9 for p in prices], y deja su entrada en paz. Si de verdad quieres que una función cambie su entrada, dilo en su nombre y su docstring (apply_sale_in_place) para que nadie se lleve una sorpresa.
Lambda y ordenar
Una lambda es una función sin nombre y de una sola expresión, escrita donde se necesita: sorted(products, key=lambda p: p['price']). La función key se llama una vez por elemento, y los elementos se ordenan por lo que devuelve. Es la misma idea que line_total, solo que lo bastante corta para escribirla en línea. Todo lo que pase de una expresión merece un def de verdad.
Funciones que llaman a funciones
Una vez que existe una función, otra puede usarla. Un resumen semanal llama a line_total por cada línea, suma los resultados y devuelve un diccionario con el total, la cuenta y la línea más grande. Cada capa confía en la de abajo, así que pruebas primero la función pequeña con unas pocas entradas cuyas respuestas puedes calcular a mano: line_total(10, 3) debería dar 30, line_total(10, 3, 50) debería dar 15, y line_total(10, 0) debería dar 0. Esas tres comprobaciones cubren el caso normal, el descuento y la línea vacía, y tardan un minuto. Los casos límite que te saltas son los que llegan a producción.
Errores comunes
"print y return hacen lo mismo"
print escribe texto en pantalla para que lo lea una persona, y el programa sigue sin él. return entrega un valor al código que llamó a la función. En un notebook los dos se ven parecidos, porque el último valor se muestra de todos modos. En el momento en que escribes x = f(...) y usas x, solo return funciona. Si una variable termina como None, sospecha de un return que falta.
"Las variables que cambio dentro de una función cambian también fuera"
Asignar un nombre dentro de una función nunca toca un nombre de fuera. Lo que sí puede escaparse es un cambio a un objeto compartido, como una lista modificada en el sitio. Una buena regla: reasignar es seguro; append, sort y p[0] = ... sobre algo que te entregaron no lo son. Ante la duda, trabaja sobre una copia con list(p) o crea una lista nueva.
"Una función debería hacer todo lo que necesita la tarea"
Las funciones pequeñas que hacen una sola cosa son más fáciles de probar y reutilizar. Un parser que limpia un precio ("$1,249.00" a 1249.0) es una función. Totalizar un export de proveedor es otra que llama a la primera. Cuando la segunda falla sabes que la primera está bien, y cuando el formato cambia editas una sola función.
Ponte a prueba
Una pregunta. Al responderla, la lección queda marcada en el mapa; nunca bloquea un nivel.
No del todo. print muestra el 30 en pantalla. No lo devuelve, así que total no lo recibe.Prueba otra respuesta.
Correcto. Una función sin return devuelve None. El 30 fue a la pantalla, y total + 5 falla entonces con un TypeError.
No del todo. total es un nombre de quien llama, al que se asigna el valor devuelto. No es local a la función. Falta el valor porque no se devolvió nada.Prueba otra respuesta.
No del todo. print funciona en cualquier sitio. La llamada se ejecuta y muestra 30; lo que se rompe es la línea que intenta usar el resultado.Prueba otra respuesta.
Lección hecha: ya está marcada en el mapa.