Funciones
En esta guía (17)
Hasta ahora usaste funciones que trae Python (print, len, sorted). En esta lección vas a escribir las tuyas. Una función es un bloque de código con nombre que recibe datos, hace algo y devuelve un resultado. Es la herramienta principal para no repetir código, dividir un problema en partes chicas y poder probar cada parte por separado.
Definir y llamar una función
def saludar(nombre):
print(f"Hola, {nombre}")
saludar("Ana")
saludar("Bruno")
Hola, Ana
Hola, Bruno
defempieza la definición; después va el nombre de la función (en snake_case), los parámetros entre paréntesis y dos puntos.- El cuerpo va indentado, como en un
if. - Definir la función no la ejecuta. Se ejecuta cuando la llamás:
saludar("Ana"). nombrees un parámetro: una variable que recibe el valor que pasás al llamar. Ese valor ("Ana") se llama argumento.
La función tiene que estar definida antes de llamarla: Python lee el archivo de arriba hacia abajo.
return: devolver un resultado
La mayoría de las funciones útiles calculan algo y lo devuelven con return:
def precio_final(precio, descuento):
return precio - precio * descuento / 100
total = precio_final(2000, 15)
print(total) # 1700.0
print(precio_final(500, 10) * 2) # 900.0
return hace dos cosas: termina la función y entrega el valor a quien la llamó. Ese valor lo podés guardar en una variable, imprimir o usar en otra cuenta.
print no es return
Es la confusión más común al empezar:
def doble_con_print(n):
print(n * 2)
def doble_con_return(n):
return n * 2
a = doble_con_print(5) # muestra 10, pero...
b = doble_con_return(5) # no muestra nada, pero...
print(a) # None
print(b) # 10
10
None
10
print muestra algo en pantalla y nada más. return devuelve un valor que el resto del programa puede usar. Una función sin return devuelve None. Regla práctica: las funciones que calculan devuelven; el print lo hace quien llama, cuando quiere mostrar el resultado.
Varios return
Una función puede tener varios return; el primero que se ejecuta la termina:
def signo(n):
if n > 0:
return "positivo"
if n < 0:
return "negativo"
return "cero"
print(signo(-4)) # negativo
print(signo(0)) # cero
No hace falta else después de un return: si la ejecución llegó a la línea siguiente, es porque la condición no se cumplió.
Parámetros con valor por defecto
Un parámetro puede tener un valor por defecto, que se usa si no le pasás ese argumento:
def formatear_precio(monto, moneda="$", decimales=2):
return f"{moneda} {monto:,.{decimales}f}"
print(formatear_precio(1234.5)) # $ 1,234.50
print(formatear_precio(1234.5, "US$")) # US$ 1,234.50
print(formatear_precio(1234.5, decimales=0)) # $ 1,234
Los parámetros con valor por defecto van después de los obligatorios.
Argumentos por posición y por nombre
Al llamar, podés pasar los argumentos por posición (en el orden de la definición) o por nombre:
def crear_usuario(nombre, edad, ciudad):
return f"{nombre} ({edad}), {ciudad}"
print(crear_usuario("Ana", 28, "Salta"))
print(crear_usuario(ciudad="Salta", nombre="Ana", edad=28))
print(crear_usuario("Ana", ciudad="Salta", edad=28))
Ana (28), Salta
Ana (28), Salta
Ana (28), Salta
Pasar por nombre hace la llamada más clara cuando hay varios parámetros, sobre todo booleanos: exportar(datos, comprimir=True) se entiende mejor que exportar(datos, True). Si mezclás, los posicionales van primero.
Devolver varios valores
Una función puede devolver varios valores separándolos con comas. En realidad devuelve una tupla, que se desempaqueta al recibirla (lección 5):
def estadisticas(numeros):
minimo = min(numeros)
maximo = max(numeros)
promedio = sum(numeros) / len(numeros)
return minimo, maximo, promedio
mn, mx, prom = estadisticas([4, 8, 15, 16, 23, 42])
print(f"min={mn} max={mx} promedio={prom:.2f}")
min=4 max=42 promedio=18.00
*args y **kwargs (introducción)
A veces querés una función que acepte cualquier cantidad de argumentos. *args los junta en una tupla:
def sumar_todo(*numeros):
total = 0
for n in numeros:
total += n
return total
print(sumar_todo(1, 2)) # 3
print(sumar_todo(5, 10, 15, 20)) # 50
print(sumar_todo()) # 0
Y **kwargs junta los argumentos por nombre en un diccionario:
def describir(**datos):
for clave, valor in datos.items():
print(f"{clave}: {valor}")
describir(nombre="Ana", rol="analista", activa=True)
nombre: Ana
rol: analista
activa: True
Lo que importa es el * y el **; los nombres args y kwargs son solo una convención. No vas a escribirlos todos los días, pero conviene reconocerlos porque aparecen mucho en código ajeno. print mismo acepta cualquier cantidad de argumentos de esta forma.
Alcance: variables locales y globales
Las variables que creás dentro de una función son locales: existen solo mientras la función se ejecuta y no se ven desde afuera.
def calcular():
resultado = 42
return resultado
calcular()
print(resultado)
NameError: name 'resultado' is not defined
Las variables definidas fuera de cualquier función son globales. Una función las puede leer:
IVA = 0.21
def con_iva(precio):
return precio * (1 + IVA)
print(con_iva(1000)) # 1210.0
Pero si asignás a ese nombre adentro de la función, Python crea una variable local nueva con el mismo nombre, y la global no cambia:
contador = 0
def incrementar():
contador = 1 # esta es una variable LOCAL, distinta de la global
return contador
incrementar()
print(contador) # 0
Existe la palabra global para modificar una global desde adentro, pero evitala: hace que el resultado de una función dependa de cosas que no se ven en su llamada y vuelve el código difícil de seguir. La alternativa es simple: pasá el valor como parámetro y devolvé el resultado.
def incrementar(contador):
return contador + 1
contador = 0
contador = incrementar(contador)
print(contador) # 1
Por convención, las constantes globales (valores que no cambian, como IVA) se escriben en MAYÚSCULAS.
Funciones puras
Una función pura cumple dos condiciones:
- Su resultado depende solo de sus parámetros.
- No modifica nada afuera: no cambia globales, no modifica las listas que recibe, no imprime.
# Impura: modifica la lista que recibe
def agregar_iva_impura(precios):
for i in range(len(precios)):
precios[i] = precios[i] * 1.21
# Pura: devuelve una lista nueva y deja la original intacta
def agregar_iva(precios):
return [p * 1.21 for p in precios]
originales = [100, 200]
nuevos = agregar_iva(originales)
print(originales) # [100, 200]
print(nuevos) # [121.0, 242.0]
Las funciones puras son más fáciles de entender, de reusar y de probar: con los mismos argumentos, siempre dan el mismo resultado. No todo puede ser puro (en algún momento hay que imprimir o guardar un archivo), pero conviene que la mayor parte de la lógica lo sea y dejar la entrada y salida en pocas funciones.
Cuidado con los valores por defecto mutables
Un valor por defecto se crea una sola vez, cuando se define la función. Si es una lista, se comparte entre llamadas:
def agregar_tarea(tarea, lista=[]): # ¡trampa!
lista.append(tarea)
return lista
print(agregar_tarea("lavar")) # ['lavar']
print(agregar_tarea("cocinar")) # ['lavar', 'cocinar'] (¿?)
La solución estándar es usar None como valor por defecto y crear la lista adentro:
def agregar_tarea(tarea, lista=None):
if lista is None:
lista = []
lista.append(tarea)
return lista
print(agregar_tarea("lavar")) # ['lavar']
print(agregar_tarea("cocinar")) # ['cocinar']
Type hints
Los type hints (anotaciones de tipo) indican qué tipo esperan los parámetros y qué tipo devuelve la función:
def precio_final(precio: float, descuento: float = 0) -> float:
return precio - precio * descuento / 100
def iniciales(nombres: list[str]) -> str:
return "".join(n[0].upper() for n in nombres)
def contar_palabras(texto: str) -> dict[str, int]:
conteo: dict[str, int] = {}
for palabra in texto.lower().split():
conteo[palabra] = conteo.get(palabra, 0) + 1
return conteo
print(precio_final(1000, 10)) # 900.0
print(iniciales(["ana", "bruno", "carla"])) # ABC
print(contar_palabras("sol y luna y sol")) # {'sol': 2, 'y': 2, 'luna': 1}
nombre: tipoanota un parámetro;-> tipoanota lo que devuelve.list[str]es "lista de strings";dict[str, int], "diccionario de claves string y valores int".- Para "puede ser un número o
None" se escribefloat | None.
Python no verifica los type hints al ejecutar: son documentación. Pero tu editor sí los usa para avisarte errores y autocompletar mejor, y hay herramientas (como mypy) que los chequean. Desde acá en adelante el curso los usa en las funciones.
Docstrings
Un docstring es un string entre triples comillas en la primera línea de la función. Explica qué hace, sin repetir el código:
def promedio(numeros: list[float]) -> float:
"""Devuelve el promedio de una lista de números.
Si la lista está vacía, devuelve 0.
"""
if not numeros:
return 0
return sum(numeros) / len(numeros)
print(promedio([7, 8, 10])) # 8.333333333333334
print(promedio([])) # 0
help(promedio) en el REPL muestra el docstring, y tu editor lo muestra cuando pasás el mouse por encima de la función. Una línea alcanza para funciones simples; para más complejas, agregá qué recibe, qué devuelve y en qué casos.
Errores comunes
- Usar
printen vez dereturn. Si después necesitás el resultado, la función te daNone. Calculá y devolvé; mostrá afuera. - Olvidar los paréntesis al llamar.
saludar(sin paréntesis) no ejecuta nada: es la función misma. Para llamarla,saludar(). - Llamar con la cantidad equivocada de argumentos. Da
TypeError: saludar() missing 1 required positional argument: 'nombre'. - Querer usar una variable local afuera de su función. Da
NameError. Devolvé el valor conreturn. - Modificar globales desde funciones. Funciona con
global, pero complica todo. Pasá parámetros y devolvé resultados. - Lista o diccionario como valor por defecto. Se comparte entre llamadas. Usá
Noney creá la colección adentro. - Funciones que hacen demasiado. Si te cuesta ponerle nombre porque hace "esto y aquello y lo otro", partila en varias.
Practicá
-
Escribí
es_bisiesto(anio: int) -> bool. Un año es bisiesto si es divisible por 4, salvo que sea divisible por 100; pero los divisibles por 400 sí son bisiestos. Probala con 2024, 1900, 2000 y 2026. -
Escribí
resumen(numeros: list[float]) -> tuple[float, float, float]que devuelva el mínimo, el máximo y el promedio, y que devuelva(0, 0, 0)si la lista está vacía. Recibí el resultado desempaquetando. -
Este código funciona, pero usa una global y modifica la lista. Reescribilo con una función pura que reciba la lista de precios y el porcentaje, y devuelva una lista nueva:
pythonprecios = [100, 250, 80] aumento = 10 def aplicar_aumento(): global precios precios = [p + p * aumento / 100 for p in precios] aplicar_aumento() print(precios) -
Escribí
validar_password(clave: str) -> list[str]que devuelva la lista de problemas encontrados (vacía si es válida): menos de 8 caracteres, sin ningún número, sin ninguna mayúscula. Pista:any(c.isdigit() for c in clave)esTruesi al menos un carácter es dígito.
Soluciones
Ejercicio 1:
def es_bisiesto(anio: int) -> bool:
"""Indica si un año es bisiesto en el calendario gregoriano."""
return anio % 4 == 0 and (anio % 100 != 0 or anio % 400 == 0)
for anio in [2024, 1900, 2000, 2026]:
print(anio, es_bisiesto(anio))
2024 True
1900 False
2000 True
2026 False
Una función que devuelve un booleano muchas veces puede devolver la condición directamente, sin if.
Ejercicio 2:
def resumen(numeros: list[float]) -> tuple[float, float, float]:
"""Devuelve (mínimo, máximo, promedio); (0, 0, 0) si la lista está vacía."""
if not numeros:
return 0, 0, 0
return min(numeros), max(numeros), sum(numeros) / len(numeros)
minimo, maximo, prom = resumen([3.5, 9, 6.5])
print(minimo, maximo, prom) # 3.5 9 6.333333333333333
print(resumen([])) # (0, 0, 0)
Ejercicio 3:
def aplicar_aumento(precios: list[float], porcentaje: float) -> list[float]:
"""Devuelve una lista nueva con los precios aumentados en el porcentaje dado."""
return [p + p * porcentaje / 100 for p in precios]
precios = [100, 250, 80]
nuevos = aplicar_aumento(precios, 10)
print(precios) # [100, 250, 80]
print(nuevos) # [110.0, 275.0, 88.0]
Ahora la función no depende de nada que no reciba, no cambia la lista original y la podés usar con cualquier lista y porcentaje.
Ejercicio 4:
def validar_password(clave: str) -> list[str]:
"""Devuelve la lista de problemas de la clave; vacía si es válida."""
problemas = []
if len(clave) < 8:
problemas.append("tiene menos de 8 caracteres")
if not any(c.isdigit() for c in clave):
problemas.append("no tiene números")
if not any(c.isupper() for c in clave):
problemas.append("no tiene mayúsculas")
return problemas
print(validar_password("hola"))
print(validar_password("Clave2026"))
['tiene menos de 8 caracteres', 'no tiene números', 'no tiene mayúsculas']
[]
Devolver la lista de problemas (en vez de solo True o False) permite que quien llama muestre mensajes útiles.
Resumen
def nombre(parametros):define una función; se ejecuta al llamarla connombre(argumentos).returntermina la función y devuelve un valor; sinreturn, devuelveNone.printno esreturn.- Los parámetros pueden tener valor por defecto, y los argumentos se pueden pasar por posición o por nombre.
return a, bdevuelve una tupla que se desempaqueta al recibirla.*argsjunta argumentos sueltos en una tupla;**kwargs, argumentos con nombre en un diccionario.- Las variables de una función son locales; evitá
globaly preferí funciones puras. - Nunca uses una lista o un diccionario como valor por defecto: usá
None. - Los type hints (
precio: float -> float) y los docstrings documentan la función para personas y herramientas.