Python · Capítulo 3 · Lección de conceptos · 6 min de lectura

Qué pasa cuando llamas a una función

Una llamada le da a la función sus propios nombres temporales, ejecuta su cuerpo con ellos y devuelve un valor con return. Todo lo demás que toque lo comparte con quien llama.

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

  1. line_total(92.99, 1, 10) la llamada
  2. price=92.99, qty=1, discount_pct=10 los argumentos pasan a ser parámetros
  3. price * qty * (1 - discount_pct / 100) se ejecuta el cuerpo
  4. return round(..., 2) -> 83.69 vuelve un solo valor

Paso 1 de 4 La línea que llama a la función le entrega tres valores, en orden. La llamada se pausa mientras la función trabaja.

  1. La línea que llama a la función le entrega tres valores, en orden. La llamada se pausa mientras la función trabaja.
  2. 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).
  3. El cuerpo se ejecuta usando solo esos nombres. 92.99 × 1 × 0.9 es 83.691. Nada fuera de la función puede ver price ni qty.
  4. return termina 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

Líneas de pedido
priceqtydiscount_pct
92.99110
349.00110
11.9940
line_total(price, qty, discount_pct)
line_total
83.69
314.10
47.96

Paso 1 de 3 Tres líneas de pedido reales. La regla del total de una línea es la misma cada vez: precio × cantidad, menos el descuento.

  1. Tres líneas de pedido reales. La regla del total de una línea es la misma cada vez: precio × cantidad, menos el descuento.
  2. Escribe la regla una vez como line_total(price, qty, discount_pct=0). El = 0 hace opcional el descuento: quien no tiene descuento puede omitirlo.
  3. 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.

Figura 3

Una función que edita la lista de quien la llama

Nombres
namepoints_to
priceslist A
plist A
list A
positionvalue
092.99
1349.00
list A después de la oferta
positionvalue
083.69
1314.10

Paso 1 de 3 prices guarda los precios de lista. Se llama a apply_sale(prices), y dentro de la función el parámetro p es un segundo nombre para la misma lista, no una copia.

  1. prices guarda los precios de lista. Se llama a apply_sale(prices), y dentro de la función el parámetro p es un segundo nombre para la misma lista, no una copia.
  2. El cuerpo hace p[0] = p[0] * 0.9 y así sucesivamente. Eso edita la propia list A, en el sitio.
  3. De vuelta en quien llama, prices sigue apuntando a list A, que ahora guarda los precios de oferta. Los precios de lista se perdieron. Construye una lista nueva dentro de la función y devuélvela con return, y el original queda a salvo.

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.

Una función se define como def line_total(price, qty): print(price * qty). Alguien escribe total = line_total(10, 3) y luego total + 5. ¿Qué falla?

Elige qué puede ejecutarse en este sitio. Puedes cambiarlo cuando quieras desde "Configurar cookies", al pie de cada página.