Clase 6a — Introducción a funciones
Hasta ahora hemos escrito código línea a línea — cada instrucción visible, cada paso explícito. Las funciones cambian eso radicalmente: permiten nombrar un bloque de código para ejecutarlo cuando se necesite, tantas veces como se quiera, desde cualquier parte del programa.
Pero las funciones no son solo una herramienta de organización. Representan una forma distinta de pensar sobre el código — dos tipos de abstracción que son la base de toda la programación moderna.
— Process abstraction: olvidarse del cómo —
La abstracción de proceso permite usar una función sin saber cómo está implementada por dentro. Solo necesitas saber qué hace — qué recibe y qué devuelve. El cómo lo hace es irrelevante.
Ya llevas seis clases usando funciones
sin haber definido ninguna:
len(), sum(),
sorted(), round(),
print().
Ninguna vez has necesitado saber
cómo Python calcula la longitud de una lista
o cómo ordena internamente.
Confías en que lo hace bien.
Eso es abstracción de proceso.
# Usamos round() sin saber cómo funciona por dentro
valor = round(88.5678, 2)
print(valor) # 88.57 — confiamos en que lo hace bien
# Usamos sorted() sin saber qué algoritmo usa
datos = [5, 2, 8, 1, 9, 3]
ordenados = sorted(datos)
print(ordenados) # [1, 2, 3, 5, 8, 9]
# Lo mismo ocurrirá con tus funciones
# Una vez definida, quien la use no necesita leer su interior
— Task decomposition: divide y vencerás —
La descomposición de tareas permite partir un problema complejo en partes más pequeñas y manejables. Cada función resuelve una parte concreta. El programa principal las combina.
Esta es exactamente la forma en que deberías usar la IA para programar: no pedir un script monolítico de 200 líneas, sino pedir funciones pequeñas que resuelvan partes concretas — y combinarlas tú. Cada función es una unidad verificable por separado.
Leer el código: ¿qué hace?
# Sin funciones: un bloque largo e indiviso
datos = [22, 25, 19, 30, 18, 28, 21]
total = 0
for d in datos:
total += d
media = total / len(datos)
maximo = datos[0]
for d in datos:
if d > maximo:
maximo = d
print(f"media: {media:.1f}, máximo: {maximo}")
# Con funciones: tareas separadas, verificables, reutilizables
def calcular_media(datos):
return sum(datos) / len(datos)
def calcular_maximo(datos):
return max(datos)
def analizar(datos):
return calcular_media(datos), calcular_maximo(datos)
media, maximo = analizar(datos)
print(f"media: {media:.1f}, máximo: {maximo}")
— Funciones e IA: el flujo correcto —
Cuando le pides a la IA que resuelva un problema complejo en un solo bloque, obtienes código difícil de verificar y difícil de corregir cuando falla. Cuando le pides funciones pequeñas con responsabilidades claras, puedes verificar cada una por separado, sustituir las que fallen y combinar las que funcionen.
La abstracción de proceso también aplica
aquí: una vez que una función está verificada,
el resto del código puede usarla
sin preocuparse de su interior.
Exactamente igual que tú usas
sorted() sin leer
su implementación.
La abstracción de proceso es
la capacidad de usar una función
conociendo solo qué hace —
sus entradas y su salida —
sin necesitar saber cómo
lo hace internamente.
Ejemplos de clases anteriores:
sorted() — ordena
una lista sin revelar el algoritmo;
sum() — suma
los elementos sin mostrar el bucle;
dict.get() — accede
con valor por defecto sin
exponer la lógica interna.
Con la versión sin funciones,
habría que copiar y pegar
el bloque completo diez veces
— con el riesgo de introducir
errores en cada copia.
Con calcular_media(datos),
basta con llamarla diez veces
con cada lista.
Si hay un error en el cálculo,
se corrige en un único lugar
y afecta a todos los usos.
Las funciones pequeñas permiten verificar cada parte por separado antes de combinarlas. Si un script de 200 líneas falla, no sabes en qué parte está el error. Si falla una función de 10 líneas, el problema está acotado. Además, si la IA genera una función incorrecta, puedes pedirle que la corrija o sustituirla sin tocar el resto.
calcular_media(datos)
dice exactamente lo que hace
sin necesidad de leer su interior.
En la Clase 2 establecimos el principio
fundamental del data model de Python:
todo en Python es un objeto.
Cada objeto tiene identidad, tipo y valor.
Desde entonces hemos trabajado con
objetos de tipo int,
str, list,
dict y más.
Ahora añadimos un tipo de objeto nuevo:
las funciones.
En Python, las funciones son objetos
como cualquier otro — tienen identidad,
tipo y valor. Pero tienen una propiedad
especial que la mayoría de objetos
no tiene: son callables,
es decir, se pueden llamar con
() para ejecutar
el código que contienen.
— Repaso: objetos, tipos e identidad —
# Todo en Python es un objeto con identidad, tipo y valor
x = 42
print(f"valor: {x}")
print(f"tipo: {type(x)}")
print(f"id: {hex(id(x))}")
# Una lista también es un objeto
lista = [1, 2, 3]
print(f"\nvalor: {lista}")
print(f"tipo: {type(lista)}")
print(f"id: {hex(id(lista))}")
# Una función también es un objeto
def saludar():
print("Hola")
print(f"\nvalor: {saludar}")
print(f"tipo: {type(saludar)}")
print(f"id: {hex(id(saludar))}")
saludar sin paréntesis
es el objeto función — una referencia
al bloque de código en memoria.
saludar() con paréntesis
ejecuta ese bloque.
Esta distinción — objeto vs ejecución —
es central en Python y en todo
el código generado por IA.
— Atributo, método y función: tres cosas distintas —
Cuando escribes objeto.algo
accedes a un atributo del objeto
— un valor asociado a él.
Cuando escribes objeto.algo()
llamas a un método —
una función que pertenece al objeto
y opera sobre él.
Cuando escribes funcion(objeto)
llamas a una función independiente
que recibe el objeto como argumento.
Veremos esto en detalle en la clase siguiente cuando hablemos de modulos
Los tres aparecen constantemente en código de Python y en código generado por IA. Saber distinguirlos permite leer cualquier script sin confusión.
lista = [3, 1, 4, 1, 5]
# ATRIBUTO: valor asociado al objeto — sin paréntesis
print(lista.__class__) #
print(lista.__class__.__name__) # 'list'
# MÉTODO: función que pertenece al objeto — con paréntesis
lista.append(9) # modifica la lista en lugar
print(lista.count(1)) # cuenta las apariciones de 1
print(lista.index(4)) # posición del 4
# FUNCIÓN: independiente — recibe el objeto como argumento
print(len(lista)) # longitud
print(sorted(lista)) # devuelve lista nueva ordenada
print(type(lista)) # tipo del objeto
# La distinción en pandas — ya la habéis visto
import pandas as pd
df = pd.DataFrame({"a": [1, 2, 3]})
print(df.shape) # atributo — sin paréntesis
print(df.columns) # atributo — sin paréntesis
print(df.head()) # método — con paréntesis
print(df["a"].mean()) # método — con paréntesis
— Qué es un callable —
Un callable es cualquier objeto
que se puede llamar con ()
para ejecutar código.
Python define esto formalmente:
un objeto es callable si implementa
el método especial __call__().
Puedes verificarlo con la función
callable().
Los callables más frecuentes son:
las funciones definidas con def,
las funciones built-in como len,
los métodos de objetos,
las clases (llamar a una clase
crea una instancia),
y cualquier objeto que tenga
__call__() definido.
# callable() verifica si un objeto se puede llamar con ()
def mi_funcion():
return 42
print(callable(mi_funcion)) # True — es callable
print(callable(len)) # True — función built-in
print(callable(print)) # True — función built-in
print(callable(list)) # True — clase (callable)
print(callable([1, 2, 3])) # False — lista no es callable
print(callable(42)) # False — int no es callable
print(callable("hola")) # False — str no es callable
# La señal visual: los paréntesis () son la ejecución
print(mi_funcion) # el objeto función — no se ejecuta
print(mi_funcion()) # se ejecuta — devuelve 42
# Error frecuente: llamar a un no-callable
try:
resultado = lista() # lista no es callable
except TypeError as e:
print(f"TypeError: {e}")
— Resumen: atributo, método y función —
| Concepto | Sintaxis | Callable | Ejemplo |
|---|---|---|---|
| Atributo |
obj.nombre
|
No |
df.shape,
lista.__class__
|
| Método |
obj.nombre()
|
Sí |
lista.append(x),
df.head()
|
| Función |
nombre(obj)
|
Sí |
len(lista),
sorted(lista)
|
| Clase |
Nombre()
|
Sí |
list(),
dict()
|
saludar?
¿Y len?
¿Tienen id?
¿Qué confirma eso sobre
su naturaleza como objetos?
texto = "Python"
print(len(texto))
print(texto.upper())
print(texto.__class__)
print(texto.replace("P", "J"))
print(type(texto))
print(callable(sorted))
print(callable(3.14))
print(callable(dict))
print(callable({"a": 1}))
print(callable(print))
df.shape
no lleva paréntesis pero
df.head() sí?
¿Qué pasaría si escribieras
df.shape()?
¿Y df.head sin paréntesis?
saludar tiene tipo
<class 'function'>.
len tiene tipo
<class 'builtin_function_or_method'>.
Ambos tienen id —
son objetos en memoria como cualquier
lista o número.
Esto confirma que en Python
las funciones son ciudadanos
de primera clase — objetos completos
con identidad, tipo y valor.
Lo desarrollaremos en detalle
en la Clase 6b.
len(texto)
→ función independiente.
texto.upper()
→ método del objeto string.
texto.__class__
→ atributo — sin paréntesis,
devuelve <class 'str'>.
texto.replace("P", "J")
→ método del objeto string.
type(texto)
→ función independiente —
equivalente a texto.__class__
pero más legible.
callable(sorted)
→ True — función built-in.
callable(3.14)
→ False — float.
callable(dict)
→ True — clase callable.
callable({"a": 1})
→ False — instancia dict.
callable(print)
→ True — función built-in.
dict la clase es callable
— llamarla crea un diccionario vacío.
{"a": 1} una instancia
de dict no es callable —
no puedes ejecutar un diccionario.
La clase y sus instancias
son objetos distintos.
df.shape es un atributo
— devuelve una tupla con las dimensiones.
No necesita paréntesis porque
no ejecuta ningún cálculo,
solo accede a un valor almacenado.
df.head() es un método
— ejecuta código que construye
y devuelve las primeras filas.
df.shape() produciría
un TypeError:
'tuple' object is not callable
— intentas llamar a una tupla
como si fuera una función.
df.head sin paréntesis
devuelve el objeto método
sin ejecutarlo —
verías algo como
<bound method DataFrame.head ...>.
Una función se define con la palabra clave
def seguida del nombre,
los parámetros entre paréntesis
y dos puntos. El cuerpo de la función
va indentado — igual que los bloques
de if y for.
La definición tiene cinco partes:
def, el nombre,
los parámetros,
el docstring opcional,
el cuerpo
y el return.
— Anatomía de una función —
# ┌── palabra clave
# │ ┌── nombre
# │ │ ┌── parámetros
# │ │ │
def area_circulo(radio):
"""Calcula el área de un círculo dado su radio.
Parámetros:
radio (float): el radio del círculo
Retorna:
float: el área del círculo
""" # ← docstring
pi = 3.1416 # ← cuerpo
return pi * radio ** 2 # ← return
El docstring es una cadena
de texto entre triple comilla
justo después de la definición.
No es un comentario — es parte del objeto
función y se puede acceder a él
desde fuera con .__doc__.
Es la forma estándar de documentar
qué hace una función, qué recibe
y qué devuelve.
— El nombre vs la llamada: objeto vs ejecución —
area_circulo sin paréntesis
es el objeto función
— una referencia al bloque de código
almacenado en memoria.
area_circulo(5) con paréntesis
ejecuta ese bloque
pasando 5 como argumento.
Esta distinción es fundamental y aparece
constantemente en código de Python.
# Sin paréntesis — el objeto función
print(area_circulo)
#
# Con paréntesis — ejecutar la función
print(area_circulo(5))
# 78.54
# Asignar la función a otra variable
# — las dos apuntan al mismo objeto
calcular_area = area_circulo
print(calcular_area(5)) # mismo resultado
print(calcular_area is area_circulo) # True
— Dunder methods: los atributos internos de una función —
En Python, todos los objetos tienen
atributos especiales cuyos nombres
empiezan y terminan con dos guiones bajos:
__nombre__.
Se llaman dunder methods
(de double underscore)
o métodos especiales.
Son la interfaz interna de Python —
definen cómo se comporta el objeto
con los operadores, las funciones
built-in y otros mecanismos del lenguaje.
Ya los hemos visto en la Clase 2
con __iter__ y
__next__ en los iterables.
Las funciones también tienen los suyos.
Los más útiles para leer e interpretar
código son tres —
los veremos en detalle en la Clase 6b,
pero vale la pena conocerlos ya.
def interes_compuesto(K, r=0.05, n=1):
"""Calcula el valor futuro con interés compuesto."""
return K * (1 + r) ** n
# __name__ — el nombre de la función como string
print(interes_compuesto.__name__)
# 'interes_compuesto'
# __doc__ — el docstring
print(interes_compuesto.__doc__)
# 'Calcula el valor futuro con interés compuesto.'
# __defaults__ — los valores por defecto de los parámetros
print(interes_compuesto.__defaults__)
# (0.05, 1) — tupla con los valores por defecto
# Sin valores por defecto → None
def suma(a, b):
return a + b
print(suma.__defaults__)
# None
# Veremos más dunder methods en la Clase 6b:
# __code__, __globals__, __annotations__, __call__
print(dir(interes_compuesto)) # todos los atributos
¿Por qué importan los dunder methods?
Porque aparecen constantemente en código
generado por IA y en código de librerías.
f.__name__ permite saber
qué función es sin ejecutarla.
f.__doc__ permite leer
la documentación desde el código.
f.__defaults__ permite
ver los valores por defecto
sin leer la definición.
Son la introspección del lenguaje —
Python leyéndose a sí mismo.
| Atributo | Qué contiene | Ejemplo |
|---|---|---|
__name__
|
Nombre de la función |
'interes_compuesto'
|
__doc__
|
Docstring |
'Calcula el valor...'
|
__defaults__
|
Valores por defecto |
(0.05, 1)
|
__code__
|
Objeto con el bytecode | Clase 6b |
__annotations__
|
Anotaciones de tipo | Clase 6b |
__call__
|
Lo que ejecuta los () |
Clase 6b |
calcular_imc que reciba
peso y altura
y devuelva el IMC.
Añade un docstring descriptivo.
Luego accede a
__name__,
__doc__ y
__defaults__.
¿Qué devuelve __defaults__
si ningún parámetro tiene valor
por defecto?
def doble(x):
return x * 2
print(doble)
print(doble(5))
print(doble.__name__)
print(type(doble))
print(callable(doble))
#?
¿Por qué el docstring es preferible
para documentar funciones?
dir(funcion)
devuelve una lista tan larga?
¿Qué nos dice eso sobre
la naturaleza de las funciones
en Python?
def calcular_imc(peso, altura):
"""Calcula el Índice de Masa Corporal.
Parámetros:
peso (float): peso en kilogramos
altura (float): altura en metros
Retorna:
float: el IMC redondeado a 2 decimales
"""
return round(peso / altura ** 2, 2)
print(calcular_imc.__name__) # 'calcular_imc'
print(calcular_imc.__doc__) # el docstring
print(calcular_imc.__defaults__) # None — sin defaults
__defaults__ devuelve
None cuando ningún
parámetro tiene valor por defecto.
Si al menos uno tiene default,
devuelve una tupla con esos valores.
print(doble) →
<function doble at 0x...>
— el objeto función.
print(doble(5)) →
10 — resultado de ejecutar.
print(doble.__name__) →
'doble'.
print(type(doble)) →
<class 'function'>.
print(callable(doble)) →
True.
Un comentario con #
es ignorado por Python —
no existe en tiempo de ejecución
y no se puede acceder a él
desde el código.
Un docstring es una cadena de texto
que Python almacena como atributo
__doc__ del objeto función.
Se puede leer programáticamente,
aparece en la ayuda interactiva
con help(funcion)
y es la forma estándar que usan
todas las librerías de Python
para documentar sus funciones.
dir(funcion) devuelve
todos los atributos del objeto —
los dunder methods más los atributos
propios. La lista es larga porque
las funciones en Python son objetos
completos con muchos atributos internos.
Nos dice que una función no es
solo código a ejecutar —
es un objeto con estado,
con atributos accesibles,
con métodos propios.
En Python no hay distinción
entre "función" y "objeto":
toda función es un objeto
del tipo function.
Hay una distinción fundamental que confunde a todos los principiantes y que produce errores difíciles de detectar: definir una función no es lo mismo que ejecutarla.
Cuando Python encuentra un bloque
def, hace exactamente
una cosa: crea el objeto función
en memoria y lo asocia
al nombre que le diste.
No entra en el cuerpo.
No ejecuta ninguna línea del interior.
No detecta ningún error que haya dentro.
Solo crea el objeto y sigue adelante.
El cuerpo se ejecuta únicamente
cuando llamas a la función con
().
Hasta ese momento el interior
de la función no existe para Python.
— def crea el objeto, () lo ejecuta —
# Paso 1: Python procesa el def
# Crea el objeto función en memoria
# No ejecuta ninguna línea del interior
def saludar(nombre):
print(f"Hola, {nombre}") # ← esta línea no se ejecuta aún
# Aquí el objeto existe pero el cuerpo nunca se ha ejecutado
print("La función existe:", saludar)
print("Pero no se ha ejecutado aún")
# Paso 2: llamamos con () — ahora sí se ejecuta el cuerpo
saludar("Ana") # ← aquí se ejecuta print(f"Hola, {nombre}")
— La consecuencia directa: errores que no se ven hasta que se llama —
Si el cuerpo de una función contiene un error — de sintaxis en tiempo de ejecución, un nombre que no existe, una operación imposible — Python no lo detecta cuando defines la función. Lo detecta solo cuando la llamas.
Esto es radicalmente distinto
a un error en código normal:
si escribes 1 / 0
en el nivel superior de un script,
falla inmediatamente al ejecutar esa línea.
Si lo escribes dentro de una función,
no falla hasta que alguien llama
a esa función.
Y si nadie la llama, el error
nunca aparece — aunque esté ahí.
# Un error en código normal — falla inmediatamente
# print(1 / 0) # ZeroDivisionError en cuanto se ejecuta esta línea
# El mismo error dentro de una función — no falla al definirla
def funcion_con_error():
return 1 / 0 # ← Python no ve esto al procesar def
# Hasta aquí: ningún error. La función existe.
print("Función definida sin error")
print(funcion_con_error)
# El error aparece solo al llamarla
try:
funcion_con_error()
except ZeroDivisionError as e:
print(f"Error al llamar: {e}")
# Si nunca la llamamos, el error nunca aparece
# aunque el código incorrecto esté ahí
— Tres tipos de error que se ocultan dentro de una función —
# Error 1: variable que no existe
def usar_variable_inexistente():
return variable_que_no_existe * 2
print("Definida sin error") # ← no falla aquí
try:
usar_variable_inexistente()
except NameError as e:
print(f"NameError al llamar: {e}")
print("---")
# Error 2: tipo incorrecto — solo falla con ciertos argumentos
def multiplicar(a, b):
return a * b
print(multiplicar(3, 4)) # 12 — funciona
print(multiplicar("x", 3)) # 'xxx' — funciona, aunque raro
try:
multiplicar([1, 2], [3, 4]) # TypeError — listas no se multiplican
except TypeError as e:
print(f"TypeError al llamar: {e}")
print("---")
# Error 3: lógica incorrecta — no lanza excepción pero el resultado es malo
def area_circulo_mal(radio):
pi = 3.1416
return pi * radio # ← falta ** 2 — error lógico silencioso
print(area_circulo_mal(5)) # 15.708 — no falla, pero es incorrecto
# El área correcta es 78.54 — nadie avisa del error
— La diferencia con código fuera de una función —
Código normal — falla inmediatamente
x = 1 / 0 # ZeroDivisionError # en esta misma línea
Dentro de función — falla al llamar
def f():
x = 1 / 0 # no falla aquí
f() # falla aquí
— Resumen: qué ocurre en cada momento —
| Momento | Qué hace Python | Errores detectados |
|---|---|---|
def funcion():
|
Crea el objeto función en memoria. Asocia el nombre. | Solo errores de sintaxis graves |
funcion(args)
|
Ejecuta el cuerpo línea a línea. Vincula argumentos a parámetros. | Todos los errores de ejecución |
def dividir(x, y):
return x / y
print("A")
print(dividir(10, 2))
print("B")
print(dividir(10, 0))
print("C")
def procesar(datos):
return datos[100] # índice que no existe
print("Script cargado correctamente")
def pct_cambio(anterior, nuevo):
return (nuevo - anterior) / anterior * 100
print(pct_cambio(100, 120)) # +20%
print(pct_cambio(100, 80)) # -20%
print(pct_cambio(0, 50)) # ¿?
ZeroDivisionError,
NameError)
y un error lógico dentro
de una función?
¿Cuál es más peligroso
en un análisis de datos?
Imprime "A",
luego 5.0,
luego "B",
luego falla con
ZeroDivisionError
al ejecutar dividir(10, 0).
El "C" nunca se imprime.
La función se definió sin error —
el fallo ocurre solo en la segunda
llamada, con los argumentos
que producen la división por cero.
No lanza error porque Python
no ejecuta el cuerpo de
procesar
al procesarlo con def.
Solo crea el objeto función.
El error aparecería al llamar
procesar(lista)
con cualquier lista
de menos de 101 elementos.
Si nadie llama nunca a
procesar,
el IndexError
nunca aparece —
aunque el código incorrecto
esté en el fichero.
La función tiene un error
silencioso: cuando
anterior = 0
produce una división por cero.
Las dos primeras llamadas
funcionan correctamente —
+20.0 y
-20.0.
La tercera lanza
ZeroDivisionError.
Es exactamente el tipo de error
que la IA no anticipa:
el código es correcto
para los casos normales
pero falla en el caso límite
anterior = 0.
La corrección:
def pct_cambio(anterior, nuevo):
if anterior == 0:
return None # o raise ValueError
return (nuevo - anterior) / anterior * 100
Un error de ejecución lanza una excepción — Python avisa, el programa se detiene y sabes exactamente dónde está el problema. Un error lógico no lanza ninguna excepción — la función devuelve un resultado incorrecto sin ningún aviso.
ZeroDivisionError
detiene el análisis y lo sabes.
Un área calculada sin elevar
al cuadrado produce un número
plausible que se propaga
a todas las conclusiones
sin que nadie lo detecte.
return:
el valor que devuelve una función
teoría
return hace dos cosas
a la vez: termina la ejecución
de la función y
devuelve un valor
al lugar donde se hizo la llamada.
Ninguna línea del cuerpo después
de un return se ejecuta.
Si una función no tiene
return — o tiene un
return sin valor —
Python devuelve None
automáticamente.
Este comportamiento silencioso es
la fuente de uno de los errores
más frecuentes en Python:
confundir print
con return.
— return termina la función y devuelve un valor —
# return termina la función — nada se ejecuta después
def clasificar(nota):
if nota >= 9:
return "sobresaliente" # sale aquí si nota >= 9
if nota >= 7:
return "notable" # sale aquí si 7 <= nota < 9
if nota >= 5:
return "aprobado" # sale aquí si 5 <= nota < 7
return "suspenso" # sale aquí si nota < 5
print(clasificar(9.5)) # sobresaliente
print(clasificar(7.2)) # notable
print(clasificar(5.0)) # aprobado
print(clasificar(3.5)) # suspenso
# El return devuelve el valor al lugar de la llamada
resultado = clasificar(8.0)
print(f"Resultado guardado: {resultado}") # notable
— Sin return la función devuelve None —
None es el valor especial
de Python que significa
"ausencia de valor".
Cuando una función no tiene
return, Python añade
implícitamente return None
al final.
Si intentas usar el resultado
de esa función — asignarlo,
sumarlo, imprimirlo como dato —
obtendrás resultados inesperados
o un TypeError.
# Función sin return — devuelve None implícitamente
def saludar(nombre):
print(f"Hola, {nombre}") # print no es return
resultado = saludar("Ana")
print(f"resultado: {resultado}") # None
print(f"tipo: {type(resultado)}") #
print("---")
# return sin valor — también devuelve None
def verificar(x):
if x < 0:
print("valor negativo")
return # sale sin devolver valor
print(f"valor: {x}")
return x * 2
print(verificar(5)) # valor: 5 → 10
print(verificar(-3)) # valor negativo → None
print("---")
# Intentar usar None como número — TypeError
try:
total = saludar("Ana") + 1
except TypeError as e:
print(f"TypeError: {e}")
— La trampa más frecuente: print vs return —
print y return
parecen hacer lo mismo porque
los dos muestran un valor.
Pero son radicalmente distintos:
print muestra el valor
en pantalla y devuelve None.
return devuelve el valor
al código que llamó la función
para que pueda usarlo.
Si solo usas print,
el valor desaparece después de mostrarse —
no se puede guardar, no se puede
pasar a otra función, no se puede
usar en un cálculo posterior.
La IA comete este error con frecuencia
cuando genera funciones — produce
resultados con print
en lugar de return.
# ❌ Con print — el valor se muestra pero no se puede usar
def area_print(radio):
pi = 3.1416
print(pi * radio ** 2) # muestra el valor, devuelve None
# ✓ Con return — el valor se puede guardar y reutilizar
def area_return(radio):
pi = 3.1416
return pi * radio ** 2 # devuelve el valor
# Diferencia en la práctica
a1 = area_print(5) # imprime 78.54, pero a1 = None
a2 = area_return(5) # a2 = 78.54
print(f"\na1: {a1}") # None
print(f"a2: {a2}") # 78.54
# Solo con return puedes encadenar resultados
radios = [1, 2, 3, 4, 5]
areas = [area_return(r) for r in radios]
print([f"{a:.2f}" for a in areas])
# Con area_print no funcionaría — devuelve lista de None
areas_mal = [area_print(r) for r in radios]
print(areas_mal) # [None, None, None, None, None]
— Retorno múltiple: devolver varios valores —
Una función puede devolver varios valores separados por coma. Python los empaqueta automáticamente en una tupla. En la llamada puedes desempaquetarlos directamente o recibirlos como tupla.
# Devolver varios valores — Python crea una tupla implícita
def estadisticas(datos):
return min(datos), max(datos), sum(datos) / len(datos)
# Recibir como tupla
resultado = estadisticas([4, 7, 2, 9, 1])
print(resultado) # (1, 9, 4.6)
print(type(resultado)) #
# Desempaquetar directamente — más legible
minimo, maximo, media = estadisticas([4, 7, 2, 9, 1])
print(f"min: {minimo}, max: {maximo}, media: {media:.1f}")
# Ignorar algún valor con _
_, maximo, _ = estadisticas([4, 7, 2, 9, 1])
print(f"solo el máximo: {maximo}")
# Patrón frecuente en pandas y sklearn
import pandas as pd
def resumen_columna(serie):
return serie.mean(), serie.std(), serie.median()
s = pd.Series([4, 7, 2, 9, 1, 5])
media, desv, mediana = resumen_columna(s)
print(f"media: {media:.2f}, desv: {desv:.2f}, mediana: {mediana:.2f}")
def f(x):
if x > 0:
return x * 2
print("negativo")
a = f(5)
b = f(-3)
print(a)
print(b)
print(a is None)
print(b is None)
def calcular_descuento(precio, pct):
descuento = precio * pct / 100
precio_final = precio - descuento
print(f"precio final: {precio_final:.2f}€")
precios = [100, 250, 80]
finales = [calcular_descuento(p, 10) for p in precios]
print(f"total: {sum(finales):.2f}€")
def separar(numeros):
pares = [n for n in numeros if n % 2 == 0]
impares = [n for n in numeros if n % 2 != 0]
return pares, impares
resultado = separar([1, 2, 3, 4, 5, 6])
print(type(resultado))
print(resultado)
print dentro
de una función en lugar de
return?
¿Y cuándo es siempre mejor
usar return?
f(5) entra en el
if, ejecuta
return 10 —
no llega al print.
a = 10.
f(-3) no entra en el
if, ejecuta
print("negativo")
y termina sin return
— devuelve None.
b = None.
Imprime: "negativo"
al ejecutar f(-3),
luego 10,
luego None,
luego False
(a no es None),
luego True
(b es None).
El error es que
calcular_descuento
usa print
en lugar de return.
Devuelve None
y la lista finales
es [None, None, None].
sum(finales)
lanza un TypeError
al intentar sumar None.
La corrección:
def calcular_descuento(precio, pct):
descuento = precio * pct / 100
return precio - descuento # ← return en lugar de print
precios = [100, 250, 80]
finales = [calcular_descuento(p, 10) for p in precios]
print(f"total: {sum(finales):.2f}€") # 387.00€
Devuelve una tupla de dos elementos:
([2, 4, 6], [1, 3, 5]).
type(resultado)
es tuple.
Aunque el return
no tiene paréntesis,
Python empaqueta los dos valores
separados por coma
en una tupla automáticamente.
pares, impares = separar(numeros).
Es más limpio que devolver
un diccionario cuando
el número de valores
es pequeño y fijo.
print tiene sentido
cuando la función es de diagnóstico
— para mostrar información
al usuario o para depurar —
y el resultado no se necesita
para nada más.
Por ejemplo, una función
mostrar_resumen(df)
que imprime estadísticas
formateadas sin devolver nada.
return es siempre
mejor cuando el resultado
se va a usar en un cálculo,
guardar en una variable,
pasar a otra función
o incluir en una comprehension.
En general: si no estás seguro,
usa return
— siempre puedes hacer
print del resultado
fuera de la función.
Hasta ahora hemos descrito lo que hace Python cuando define y llama una función — la creación del objeto, el frame, la vinculación de argumentos. Python Tutor permite verlo ocurrir paso a paso: qué variables existen en cada momento, dónde viven, cuándo aparecen y cuándo desaparecen.
Es la herramienta más útil para construir el modelo mental correcto de cómo funciona Python — y para detectar exactamente dónde falla un programa.
— Cómo usar Python Tutor —
Entra en pythontutor.com , haz clic en Start visualizing your code now, selecciona Python 3, pega el código y haz clic en Visualize Execution. Luego usa los botones Next y Prev para avanzar y retroceder paso a paso.
En cada paso observa tres zonas: el código con la línea actual resaltada, el stack con los frames activos y sus variables locales, y el heap con los objetos en memoria.
— Qué buscar en cada paso —
Al definir con def
El objeto función aparece en el heap. El nombre aparece en el frame global apuntando al objeto. El cuerpo no se ejecuta.
Al llamar con ()
Aparece un frame nuevo en el stack con los parámetros vinculados a los argumentos. El cuerpo empieza a ejecutarse.
Al ejecutar return
El frame desaparece del stack. Las variables locales dejan de existir. El valor vuelve al frame que hizo la llamada.
— Caso 1: definición y llamada básica —
El caso más simple: una función
con un parámetro y un return.
Observa cuándo aparece el frame
de area_circulo
y cuándo desaparece.
Comprueba que pi
y radio solo existen
dentro del frame de la función.
Copia y pega esta función en Python Tutor y dale a Visualize Execution y Next
def area_circulo(radio):
pi = 3.1416
return pi * radio ** 2
area_circulo(4)
Qué observar paso a paso:
def:
aparece area_circulo
en el frame global apuntando
al objeto función en el heap.area_circulo(4):
aparece un frame nuevo con
radio = 4.
pi = 3.1416:
aparece pi
dentro del frame — no en el global.return:
el frame desaparece.
pi y radio
ya no existen.
— Caso 2: guardar el resultado en una variable —
Mismo código pero guardando
el resultado en area4.
Observa cómo el valor devuelto
por return pasa
al frame global y se asigna
a area4.
def area_circulo(radio):
pi = 3.1416
return pi * radio ** 2
area4 = area_circulo(4)
Qué observar paso a paso:
return.return:
el valor pasa al frame global
y se asigna a area4.area4
persiste en el frame global.— Caso 3: variable local para el resultado —
Introducimos area
como variable local antes del
return.
Observa que area
existe en el frame de la función
pero desaparece al hacer
return — solo el valor
que devuelve return
llega al frame global (local y global lo iremos viendo en los sucesivo, e.g.,
scope, pero quedate con la duda y a ver si te la resuelvo...).
def area_circulo(radio):
pi = 3.1416
area = pi * radio ** 2
return area
area4 = area_circulo(4)
Qué observar paso a paso:
area
dentro del frame de la función
— es local, no global.return area:
el valor de area
se copia al frame global
como area4.area,
pi y radio
desaparecen con el frame.
Solo area4 persiste.
— Caso 4: un error dentro de la función —
Mismo código pero con un error
tipográfico deliberado:
'pi' como string
en lugar de la variable pi.
Observa en qué paso aparece el error
y compáralo con el Caso 1 —
la función se define sin problema,
el error solo aparece al ejecutar
la línea incorrecta.
def area_circulo(radio):
pi = 3.1416
area = 'pi' * radio ** 2 # 'pi' es un string, no la variable
return area
area4 = area_circulo(4)
Qué observar paso a paso:
def:
ningún error — la función
se crea normalmente.area_circulo(4):
el frame aparece con
radio = 4.
area = 'pi' * radio ** 2:
'pi' * 16
produce 'pipipi...'
— no un error, sino un resultado
inesperado.
area_circulo?
¿En qué paso desaparece?
area
durante la ejecución?
¿Puedes acceder a ella
desde fuera de la función
después de que termine?
¿Por qué?
area4
al terminar?
¿Por qué Python no lanza
ninguna excepción aunque
el resultado es incorrecto?
Conecta con lo visto en
el Bloque 2 sobre errores lógicos.
return.
Llámala dos veces
con argumentos distintos.
Observa que en cada llamada
se crea un frame nuevo
con sus propias variables.
El Caso 1 tiene 5 pasos:
procesar def,
llamar area_circulo(4)
con creación del frame,
ejecutar pi = 3.1416,
ejecutar return
y volver al frame global.
El frame de area_circulo
aparece en el paso 2
— al hacer la llamada —
y desaparece en el paso 4
— al ejecutar return.
area vive
en el frame local
de area_circulo
— solo existe mientras
la función se está ejecutando.
No se puede acceder a ella
desde fuera después
de que termine porque
el frame desaparece
con todas sus variables.
Intentar acceder con
print(area)
fuera de la función
produciría un
NameError.
Esto es el concepto de
scope local que veremos
en detalle en la Clase 6b.
area4 vale
'pipipipipipipipipipipipipipipipi'
— el string 'pi'
repetido 16 veces
(radio ** 2 = 16).
Python no lanza excepción
porque multiplicar un string
por un entero es una operación
válida en Python —
repite el string.
Es un error lógico silencioso:
el código es sintácticamente
correcto y no viola
ninguna regla de tipos,
pero el resultado
no es el esperado.
Solo lo detectas si
verificas el resultado —
o si usas Python Tutor
para ver el valor en cada paso.
print de diagnóstico
en cada línea.
Al llamar la función dos veces, Python Tutor muestra que cada llamada crea su propio frame con sus propias variables locales — completamente independiente de la llamada anterior. Los parámetros se vinculan a los nuevos argumentos y las variables locales empiezan desde cero. Esto demuestra que las variables locales de una función no persisten entre llamadas — cada ejecución es un estado limpio.
Dos términos que se confunden constantemente pero que significan cosas distintas: un parámetro es el nombre de variable que aparece en la definición de la función — es un placeholder, un nombre reservado para recibir un valor. Un argumento es el valor concreto que se pasa a la función en la llamada.
Cuando llamas a una función, Python vincula cada argumento a su parámetro correspondiente — el argumento pasa a ser el valor de esa variable local dentro del frame de la función.
# parámetros: base y exponente — nombres en la definición
def potencia(base, exponente):
return base ** exponente
# argumentos: 2 y 3 — valores en la llamada
resultado = potencia(2, 3)
print(resultado) # 8
# Los parámetros solo existen dentro de la función
# Fuera, solo existe el resultado devuelto por return
try:
print(base) # NameError — base no existe fuera
except NameError as e:
print(f"NameError: {e}")
— Argumentos posicionales: el orden importa —
Un argumento posicional se vincula a su parámetro por la posición en que aparece — el primero al primer parámetro, el segundo al segundo, y así sucesivamente. No se nombra explícitamente. El orden es lo único que determina qué valor recibe cada parámetro.
def dividir(x, y):
return x / y
# El orden determina qué recibe cada parámetro
print(dividir(10, 2)) # x=10, y=2 → 5.0
print(dividir(2, 10)) # x=2, y=10 → 0.2
print(dividir(0, 10)) # x=0, y=10 → 0.0
# Error de orden — resultado incorrecto sin aviso
def restar(total, descuento):
return total - descuento
print(restar(100, 20)) # 80 — correcto
print(restar(20, 100)) # -80 — incorrecto, sin error
# Tres reglas de los posicionales:
# 1. Se pasan en el orden de la definición
# 2. Son obligatorios salvo que tengan default
# 3. No se nombran explícitamente
— Argumentos por defecto: parámetros opcionales —
Un argumento por defecto es un parámetro con un valor predefinido en la definición. Si la llamada no proporciona ese argumento, Python usa el valor por defecto. Si lo proporciona, el valor por defecto se ignora.
Regla sintáctica obligatoria: los parámetros con valor por defecto deben ir siempre después de los parámetros sin default. Si no, Python no puede determinar qué argumentos se han pasado y cuáles hay que inferir del default.
def interes_compuesto(K, r=0.05, n=1):
"""Calcula el valor futuro con interés compuesto."""
return K * (1 + r) ** n
# Solo el obligatorio — r y n usan defaults
print(interes_compuesto(1000))
# K=1000, r=0.05, n=1 → 1050.0
# Sobreescribir n por nombre
print(interes_compuesto(1000, n=5))
# K=1000, r=0.05, n=5 → 1276.28
# Sobreescribir r por posición — n sigue con default
print(interes_compuesto(1000, 0.1))
# K=1000, r=0.1, n=1 → 1100.0
# Todos explícitos
print(interes_compuesto(1000, 0.1, 3))
# K=1000, r=0.1, n=3 → 1331.0
print("---")
# La regla: defaults siempre al final
# ✓ Correcto
def f(x, y=0):
return x + y
# ❌ SyntaxError — default antes que no-default
try:
exec("def g(x=0, y): return x + y")
except SyntaxError as e:
print(f"SyntaxError: {e}")
restar(20, 100)
en lugar de restar(100, 20)
no produce ningún error —
Python simplemente vincula
los valores en el orden equivocado
y devuelve un resultado incorrecto.
Con funciones de muchos parámetros
este error es muy difícil de detectar.
Los keyword arguments — que veremos
en el Bloque 2 — son la solución.
def saludar(nombre, saludo="Hola"):
return f"{saludo}, {nombre}"
print(saludar("Ana"))
print(saludar("Bob", "Buenos días"))
def interes_compuesto(K, r=0.05, n=1):
return round(K * (1 + r) ** n, 2)
print(interes_compuesto(1000))
print(interes_compuesto(1000, n=5))
print(interes_compuesto(1000, 5))
print(interes_compuesto(1000, 0.5, 10))
Atención al tercer caso:
¿qué valor recibe r?
SyntaxError?
¿Cómo la corregirías?
def crear_usuario(nombre, rol="usuario", edad):
return {"nombre": nombre,
"rol": rol,
"edad": edad}
def calcular_ipc(precio_actual, precio_base, año=2024):
return (precio_actual / precio_base) * 100
# Llamada correcta
print(calcular_ipc(115, 100))
# Llamada con argumentos invertidos
print(calcular_ipc(100, 115))
Parámetros: nombre
y saludo —
son los nombres en la definición.
saludo tiene
valor por defecto
"Hola".
Primera llamada: argumentos
"Ana"
— saludo usa
el default.
Resultado:
"Hola, Ana".
Segunda llamada: argumentos
"Bob"
y "Buenos días"
— el default se ignora.
Resultado:
"Buenos días, Bob".
interes_compuesto(1000)
→ 1050.0
— r=0.05,
n=1 por defecto.
interes_compuesto(1000, n=5)
→ 1276.28
— r=0.05 por defecto,
n=5 explícito.
interes_compuesto(1000, 5)
→ 6000.0
— r=5 por posición,
n=1 por defecto.
El tercer caso es la trampa:
el 5 se asigna
a r por posición,
no a n.
interes_compuesto(1000, 0.5, 10)
→ 57665.04
— todos explícitos.
Produce SyntaxError
porque edad
— sin default —
aparece después de
rol — con default.
Python no puede determinar
si el segundo argumento
en la llamada es para
rol o para
edad.
La corrección es poner
los parámetros sin default
primero:
def crear_usuario(nombre, edad, rol="usuario"):
return {"nombre": nombre,
"rol": rol,
"edad": edad}
Llamada correcta:
precio_actual=115,
precio_base=100
→ 115.0 — correcto.
Llamada invertida:
precio_actual=100,
precio_base=115
→ 86.96
— incorrecto pero sin error.
El IPC debería ser 115
pero obtenemos 86.96
porque estamos dividiendo
al revés.
Es un error silencioso
típico de posicionales
con nombres similares —
el resultado es plausible
y nadie avisa del problema.
calcular_ipc(precio_actual=115,
precio_base=100)
es imposible de invertir
accidentalmente.
Lo veremos en el Bloque 2.
*
y
**
práctica
Un keyword argument es un argumento que se pasa por nombre en lugar de por posición. En lugar de confiar en el orden, especificas explícitamente qué parámetro recibe qué valor. Esto elimina los errores de orden silenciosos que vimos en el Bloque 1 y hace el código mucho más legible.
— Pasar argumentos por nombre —
def interes_compuesto(K, r=0.05, n=1):
"""Calcula el valor futuro con interés compuesto."""
return round(K * (1 + r) ** n, 2)
# Por posición — el orden importa
print(interes_compuesto(1000, 0.1, 3))
# Por nombre — el orden no importa
print(interes_compuesto(K=1000, r=0.1, n=3))
print(interes_compuesto(n=3, r=0.1, K=1000)) # mismo resultado
# Mezcla — posicionales primero, keywords después
print(interes_compuesto(1000, n=5)) # K por posición, n por nombre
print(interes_compuesto(1000, r=0.1)) # K por posición, r por nombre
# ❌ Keyword antes que posicional — SyntaxError
try:
exec("interes_compuesto(K=1000, 0.1)")
except SyntaxError as e:
print(f"SyntaxError: {e}")
# La solución al error de orden del Bloque 1
def calcular_ipc(precio_actual, precio_base, año=2024):
return (precio_actual / precio_base) * 100
# Imposible de invertir accidentalmente
print(calcular_ipc(precio_actual=115, precio_base=100))
— Unpacking: extraer valores de colecciones —
El unpacking permite extraer valores de una secuencia o diccionario y asignarlos directamente a variables. Ya lo vimos en la Clase 3 con tuplas y listas. Ahora lo aplicamos a los argumentos de una función: en lugar de pasar los argumentos uno a uno, pasas directamente una colección y Python la desempaqueta.
* desempaqueta secuencias
— listas y tuplas — como argumentos
posicionales.
** desempaqueta diccionarios
como keyword arguments.
# Repaso de unpacking de la Clase 3
punto = (3, 5)
x, y = punto
print(x, y) # 3 5
valores = (1, 2, 3, 4)
a, b, _, d = valores
print(a, b, d) # 1 2 4
a, *b = [10, 20, 30, 40]
print(a, b) # 10 [20, 30, 40]
print("---")
# * unpacking en argumentos de función
def suma(a, b):
return a + b
s = (4, 5)
print(suma(*s)) # equivale a suma(4, 5)
nums = [1, 2, 3]
def sumar_tres(a, b, c):
return a + b + c
print(sumar_tres(*nums)) # equivale a sumar_tres(1, 2, 3)
print("---")
# ** unpacking en argumentos de función
def interes_compuesto(K, r=0.05, n=1):
return round(K * (1 + r) ** n, 2)
d = {"r": 0.4, "n": 10}
print(interes_compuesto(1000, **d))
# equivale a interes_compuesto(1000, r=0.4, n=10)
# Diccionario completo — todas las claves como keywords
params = {"K": 2000, "r": 0.08, "n": 5}
print(interes_compuesto(**params))
# equivale a interes_compuesto(K=2000, r=0.08, n=5)
— Patrones frecuentes con unpacking —
# ** para pasar configuraciones como diccionario
def saludo(nombre, mensaje):
print(f"{mensaje}, {nombre}!")
datos = {"nombre": "Ana", "mensaje": "Hola"}
saludo(**datos) # Hola, Ana!
print("---")
# Patrón frecuente en análisis de datos:
# guardar parámetros de un modelo en un dict
def ajustar_modelo(datos, learning_rate=0.01,
epochs=100, batch_size=32):
print(f"lr={learning_rate}, epochs={epochs}, "
f"batch={batch_size}")
# ... entrenamiento
# Configuración guardada como dict — fácil de modificar
config = {
"learning_rate": 0.001,
"epochs": 50,
"batch_size": 64
}
ajustar_modelo([1, 2, 3], **config)
print("---")
# * para pasar lista como argumentos posicionales
# Patrón frecuente con zip y enumerate
pares = [(1, 10), (2, 20), (3, 30)]
for par in pares:
print(suma(*par)) # equivale a suma(par[0], par[1])
def describir(nombre, edad, ciudad="desconocida"):
return f"{nombre}, {edad} años, {ciudad}"
print(describir("Ana", 28))
print(describir(edad=28, nombre="Ana"))
datos = ["Bob", 35, "Madrid"]
print(describir(*datos))
config = {"nombre": "Eva", "edad": 22}
print(describir(**config))
print producen
el mismo resultado aunque
los argumentos están
en distinto orden?
¿Qué ventaja tiene
pasar argumentos por nombre
cuando una función tiene
muchos parámetros?
** para pasar
parámetros de configuración.
¿Qué imprime? ¿Qué ventaja
tiene este patrón sobre
pasar los argumentos
uno a uno?
def graficar(titulo, color="blue",
linewidth=1, alpha=1.0):
print(f"título: {titulo}")
print(f"color: {color}, lw: {linewidth}, alpha: {alpha}")
estilo = {"color": "red", "linewidth": 2, "alpha": 0.8}
graficar("Ventas 2024", **estilo)
graficar("Costes 2024", **estilo)
def f(a, b, c):
return a + b + c
nums = [1, 2, 3, 4]
print(f(*nums))
describir("Ana", 28)
→ "Ana, 28 años, desconocida"
— ciudad usa default.
describir(edad=28, nombre="Ana")
→ "Ana, 28 años, desconocida"
— mismo resultado, orden distinto.
describir(*datos)
→ "Bob, 35 años, Madrid"
— la lista se desempaqueta
como posicionales.
describir(**config)
→ "Eva, 22 años, desconocida"
— el dict se desempaqueta
como keywords,
ciudad usa default.
El segundo y tercer
print producen
el mismo resultado porque
los keyword arguments
se vinculan por nombre —
el orden en que aparecen
en la llamada no importa,
solo importa qué nombre
corresponde a qué parámetro.
Con funciones de muchos parámetros,
los keyword arguments
eliminan la necesidad de recordar
el orden exacto de la definición
y hacen el código autoexplicativo.
Imprime para cada llamada:
título: Ventas 2024
color: red, lw: 2, alpha: 0.8
título: Costes 2024
color: red, lw: 2, alpha: 0.8
La ventaja es que el estilo está definido en un solo lugar. Si quieres cambiar el color de todos los gráficos, solo modificas el dict en un punto — no tienes que buscar y cambiar cada llamada. Es el mismo principio que reutilizar una función en lugar de copiar código.
Produce un
TypeError: f() takes 3
positional arguments but 4
were given.
*nums desempaqueta
la lista completa
como cuatro argumentos posicionales
— f(1, 2, 3, 4)
— pero f
solo acepta tres.
La corrección: usar solo
los tres primeros elementos
f(*nums[:3])
o definir la función
para aceptar un número
variable de argumentos
con *args
— que veremos en la Parte IV.
Los tres tipos de argumentos que hemos visto — posicionales, por defecto y keyword — pueden combinarse en una misma llamada. Pero hay reglas estrictas sobre el orden en que pueden aparecer. Conocerlas permite leer cualquier llamada a función sin confusión y detectar exactamente por qué falla un código generado por IA.
— Reglas de orden en la llamada —
En una llamada a función los argumentos deben seguir este orden: primero los posicionales, luego los keyword. Un posicional nunca puede aparecer después de un keyword — Python no sabría a qué parámetro asignarlo.
def f(a, b, c=0, d=0):
return f"a={a}, b={b}, c={c}, d={d}"
# ✓ Posicionales primero, keywords después
print(f(1, 2)) # a=1, b=2, c=0, d=0
print(f(1, 2, c=3)) # a=1, b=2, c=3, d=0
print(f(1, 2, c=3, d=4)) # a=1, b=2, c=3, d=4
print(f(1, 2, d=4)) # a=1, b=2, c=0, d=4
print(f(a=1, b=2, c=3, d=4)) # todos por nombre
# ❌ Errores de orden
try:
exec("f(1, b=2, 3)") # posicional después de keyword
except SyntaxError as e:
print(f"SyntaxError: {e}")
# ❌ Argumento repetido
try:
f(1, 2, a=10) # a recibe valor dos veces
except TypeError as e:
print(f"TypeError: {e}")
# ❌ Demasiados argumentos
try:
f(1, 2, 3, 4, 5) # más argumentos que parámetros
except TypeError as e:
print(f"TypeError: {e}")
# ❌ Faltan argumentos obligatorios
try:
f(1) # falta b — sin default
except TypeError as e:
print(f"TypeError: {e}")
— Errores frecuentes en código generado por IA —
La IA comete tres errores frecuentes
con los argumentos:
confundir el orden de parámetros
con valores por defecto similares,
pasar un argumento por nombre
con un nombre incorrecto,
y usar ** con un diccionario
cuyas claves no coinciden exactamente
con los parámetros.
Los tres producen errores
que solo aparecen al llamar la función.
# Error 1: nombre de keyword incorrecto
def calcular(precio, cantidad, descuento=0):
return precio * cantidad * (1 - descuento / 100)
try:
calcular(10, 5, descuentos=20) # 'descuentos' no existe
except TypeError as e:
print(f"TypeError: {e}")
print("---")
# Error 2: claves del dict no coinciden con parámetros
def crear_perfil(nombre, edad, ciudad):
return {"nombre": nombre, "edad": edad, "ciudad": ciudad}
datos = {"nombre": "Ana", "age": 28, "ciudad": "Madrid"}
try:
crear_perfil(**datos) # 'age' no es un parámetro válido
except TypeError as e:
print(f"TypeError: {e}")
print("---")
# Error 3: argumento obligatorio con nombre de default similar
def modelo(X, y, test_size=0.2, random_state=42):
print(f"test={test_size}, seed={random_state}")
# La IA a veces confunde el orden de X e y
# — ambos son obligatorios y sin nombres obvios
modelo([1, 2, 3], [0, 1, 0]) # correcto
modelo(X=[1, 2, 3], y=[0, 1, 0]) # explícito — más seguro
modelo([1, 2, 3], [0, 1, 0],
random_state=0, test_size=0.3) # keywords en cualquier orden
— Leer mensajes de error de argumentos —
Python es muy preciso en sus mensajes de error sobre argumentos. Leerlos bien permite identificar el problema exacto sin depurar línea a línea.
def procesar(datos, umbral, metodo="media"):
pass
# Cada error tiene un mensaje distinto y preciso
# "missing N required positional argument(s): 'x'"
# → falta un argumento obligatorio
try:
procesar([1, 2])
except TypeError as e:
print(f"1: {e}")
# "takes N positional arguments but M were given"
# → demasiados argumentos
try:
procesar([1], 5, "media", "extra")
except TypeError as e:
print(f"2: {e}")
# "got an unexpected keyword argument 'x'"
# → nombre de keyword incorrecto
try:
procesar([1], 5, metodos="media")
except TypeError as e:
print(f"3: {e}")
# "got multiple values for argument 'x'"
# → argumento pasado dos veces
try:
procesar([1], 5, datos=[2])
except TypeError as e:
print(f"4: {e}")
def analizar(df, columna, metodo="media", redondear=True):
pass
analizar()
analizar("df", "col", "media", True, "extra")
analizar("df", "col", metodos="media")
analizar("df", columna="col", columna="col2")
**
para pasar parámetros pero tiene
un error. Identifícalo
y corrígelo:
def filtrar(datos, minimo, maximo, incluir_bordes=True):
pass
params = {
"datos": [1, 2, 3],
"min": 0,
"max": 10,
"incluir_bordes": True
}
filtrar(**params)
def informe(titulo, año, trimestre=1, publicar=False):
print(f"{titulo} — {año} T{trimestre} — pub: {publicar}")
# Queremos: titulo="Ventas", año=2024, trimestre=3, publicar=True
def normalizar(datos, metodo="minmax", rango=(0, 1)):
pass
config = {"method": "zscore", "rango": (-1, 1)}
normalizar(**config)
normalizar(metodo="minmax", [1, 2, 3])
normalizar([1, 2, 3], "zscore", (0, 1), True)
analizar()
→ TypeError: missing 2
required positional arguments:
'df' and 'columna'
— faltan los dos obligatorios.
analizar("df", "col",
"media", True, "extra")
→ TypeError: takes from
2 to 4 positional arguments
but 5 were given
— un argumento de más.
analizar("df", "col",
metodos="media")
→ TypeError: got an
unexpected keyword argument
'metodos'
— nombre incorrecto
(metodos
en lugar de
metodo).
analizar("df",
columna="col",
columna="col2")
→ SyntaxError:
keyword argument repeated
— columna
aparece dos veces.
Las claves "min"
y "max" no coinciden
con los parámetros
minimo
y maximo.
Produce:
TypeError: got an
unexpected keyword argument
'min'.
La corrección es usar
las claves correctas:
params = {
"datos": [1, 2, 3],
"minimo": 0, # ← corregido
"maximo": 10, # ← corregido
"incluir_bordes": True
}
filtrar(**params)
# Completamente posicional
informe("Ventas", 2024, 3, True)
# Completamente por nombre
informe(titulo="Ventas", año=2024,
trimestre=3, publicar=True)
# Mixta — posicionales primero
informe("Ventas", 2024, trimestre=3, publicar=True)
Las tres producen el mismo output:
Ventas — 2024 T3 — pub: True.
La versión por nombre es la más
legible y la más resistente
a cambios en la definición.
Tres problemas:
Primera llamada
normalizar(**config):
la clave "method"
no coincide con el parámetro
"metodo"
— TypeError.
Además falta "datos"
— argumento obligatorio.
Segunda llamada
normalizar(metodo="minmax",
[1, 2, 3]):
argumento posicional
después de keyword
— SyntaxError.
Tercera llamada
normalizar([1,2,3],
"zscore", (0,1), True):
cuatro argumentos pero la función
solo acepta tres —
TypeError.
**dict
coinciden exactamente,
y comprueba que no hay
posicionales después
de keywords.
Estos tres checks eliminan
la mayoría de errores
de argumentos.
En la Parte II usamos Python Tutor para ver cuándo se crea el frame y cuándo desaparece. Ahora lo usamos para ver cómo se vinculan los argumentos a los parámetros en el frame — qué valor recibe cada parámetro, cuándo aparece el valor por defecto y qué ocurre con el unpacking.
Los cuatro casos son exactamente
los de tus slides —
la misma función
interes_compuesto
llamada de cuatro formas distintas.
Observa en cada caso qué variables
aparecen en el frame y con qué valores.
Entra en
pythontutor.com
,
pega cada caso y avanza paso a paso.
Fíjate especialmente en el frame
de interes_compuesto:
qué variables aparecen,
qué valores tienen
y si los valores por defecto
aparecen o no según la llamada.
— Caso 1: solo el argumento obligatorio —
Solo se pasa K.
Los parámetros r y
n usan sus valores
por defecto.
Observa que en el frame aparecen
los tres parámetros —
aunque solo se pasó uno.
def interes_compuesto(K, r=0.05, n=1):
"""Calcula el valor futuro con interés compuesto."""
return K * (1 + r) ** n
i_c = interes_compuesto(1000)
Qué observar:
K=1000,
r=0.05,
n=1
— los tres vinculados aunque
solo se pasó uno.
r y n
si no se pasaron?return:
¿qué valor se asigna a
i_c en el frame global?
— Caso 2: sobreescribir un default por nombre —
Se pasa K por posición
y n por nombre.
r usa su default.
Observa que en el frame
n tiene el valor
pasado explícitamente
aunque no es el segundo parámetro
en la definición.
def interes_compuesto(K, r=0.05, n=1):
"""Calcula el valor futuro con interés compuesto."""
return K * (1 + r) ** n
i_c = interes_compuesto(1000, n=5)
Qué observar:
K=1000,
r=0.05,
n=5.
¿Por qué r sigue
siendo 0.05
aunque no se pasó?
i_c
al final?— Caso 3: la trampa del posicional —
Se pasan K=1000
y 5 por posición.
El 5 va al segundo
parámetro en orden — que es
r, no n.
Este es exactamente el error
silencioso del Bloque 1:
el código no falla pero
el resultado es incorrecto.
def interes_compuesto(K, r=0.05, n=1):
"""Calcula el valor futuro con interés compuesto."""
return K * (1 + r) ** n
i_c = interes_compuesto(1000, 5)
Qué observar:
r?
¿Y n?
Compara con el Caso 2.— Caso 4: unpacking con ** —
Los argumentos se pasan
desde un diccionario con
**.
Observa que el frame muestra
los mismos valores que
si se hubieran pasado
como keywords individuales —
Python desempaqueta el dict
antes de vincular los argumentos.
def interes_compuesto(K, r=0.05, n=1):
"""Calcula el valor futuro con interés compuesto."""
return K * (1 + r) ** n
d = {"r": 0.4, "n": 10}
i_c = interes_compuesto(1000, **d)
Qué observar:
K=1000,
r=0.4,
n=10.
¿Cómo llegaron los valores
del diccionario al frame?
K, r
y n en el frame
y qué valor devuelve
la función.
¿Coinciden con tus predicciones
del Bloque 1?
i_c?
¿Por qué es tan alto?
¿Cómo reescribirías la llamada
para que n=5
y r=0.05?
"tasa": 0.1
en lugar de "r": 0.4
— y ejecuta en Python Tutor.
¿En qué paso aparece el error?
¿Qué dice el mensaje?
**dict.
Observa en los tres casos
qué aparece en el frame.
Caso 1: K=1000,
r=0.05,
n=1
→ i_c = 1050.0.
Caso 2: K=1000,
r=0.05,
n=5
→ i_c = 1276.28.
Caso 3: K=1000,
r=5,
n=1
→ i_c = 6000.0.
Caso 4: K=1000,
r=0.4,
n=10
→ i_c = 28925.27.
i_c = 6000.0
porque r=5
— un tipo de interés del 500% —
en lugar del 5% esperado.
El resultado es enorme
pero plausible como número,
por eso no lanza error.
La versión correcta
para n=5
y r=0.05:
# Por nombre — imposible de confundir
i_c = interes_compuesto(1000, n=5)
# K=1000, r=0.05 (default), n=5 → 1276.28
El error aparece
al llamar la función —
en el momento en que Python
intenta desempaquetar el dict
y vincularlo a los parámetros.
El mensaje es:
TypeError: got an
unexpected keyword argument
'tasa'.
No falla al definir el dict
ni al ejecutar el cuerpo —
falla exactamente en la llamada,
antes de que el frame
se cree completamente.
En los tres casos Python Tutor muestra el mismo frame con los tres parámetros vinculados a sus valores — independientemente de cómo se pasaron. Esto confirma que el frame es el estado interno de la función: Python resuelve la vinculación antes de entrar en el cuerpo, y el cuerpo siempre ve los mismos nombres con sus valores ya asignados.
*args:
número variable de argumentos posicionales
teoría
Hasta ahora todas las funciones
que hemos definido tienen un número
fijo de parámetros — el programador
decide cuántos al escribir el
def.
Pero hay situaciones donde
no se sabe de antemano cuántos
argumentos recibirá la función.
Para eso existe *args.
*args en la definición
de una función captura todos los
argumentos posicionales extra
— los que no tienen parámetro
asignado — y los empaqueta
en una tupla.
Dentro de la función,
args es simplemente
una tupla con esos valores.
El nombre args es
una convención — puedes usar
cualquier nombre con un
* delante.
Lo que importa es el asterisco,
no el nombre.
— El * tiene dos usos opuestos —
* en la llamada — desempaqueta
nums = [1, 2, 3] f(*nums) # equivale a f(1, 2, 3) # expande la lista en argumentos
* en la definición — empaqueta
def f(*args):
print(args)
f(1, 2, 3)
# args = (1, 2, 3)
# recoge argumentos en tupla
— *args básico —
# Sin *args — número fijo de parámetros
def sumar_dos(a, b):
return a + b
# Con *args — número variable
def sumar_todo(*args):
print(f"args es: {args}")
print(f"tipo: {type(args)}")
return sum(args)
print(sumar_todo(1, 2)) # args = (1, 2)
print(sumar_todo(1, 2, 3, 4, 5)) # args = (1, 2, 3, 4, 5)
print(sumar_todo(10)) # args = (10,)
print(sumar_todo()) # args = () — tupla vacía
— *args combinado con parámetros normales —
*args debe ir
después de todos
los parámetros posicionales normales.
Los parámetros antes de *args
reciben sus argumentos primero —
los restantes van a la tupla.
def mostrar(a, b, *args):
print(f"a={a}, b={b}")
print(f"resto: {args}")
mostrar(1, 2) # a=1, b=2, args=()
mostrar(1, 2, 3) # a=1, b=2, args=(3,)
mostrar(1, 2, 3, 4, 5) # a=1, b=2, args=(3, 4, 5)
print("---")
# Iterar sobre args — es una tupla normal
def imprimir_todos(titulo, *args):
print(f"=== {titulo} ===")
for i, valor in enumerate(args, start=1):
print(f" {i}. {valor}")
imprimir_todos("Frutas", "manzana", "pera", "kiwi")
imprimir_todos("Vacío") # args = () — no imprime nada
— Ejemplo completo: operación sobre múltiples números —
def operar(operacion, *numeros, redondear=True):
"""
Aplica una operación a múltiples números.
operacion: 'suma', 'multiplicacion', 'promedio'
*numeros: números a procesar
redondear: si se redondea el resultado
"""
if not numeros:
return None
if operacion == "suma":
resultado = sum(numeros)
elif operacion == "multiplicacion":
resultado = 1
for n in numeros:
resultado *= n
elif operacion == "promedio":
resultado = sum(numeros) / len(numeros)
else:
return "operación no soportada"
return round(resultado, 2) if redondear else resultado
print(operar("suma", 1, 2, 3))
print(operar("promedio", 2, 2, 3, 5))
print(operar("promedio", 2, 2, 3, 5, redondear=False))
print(operar("multiplicacion", 2, 3, 4))
print(operar("suma")) # numeros vacío → None
Observa la firma de operar:
operacion, *numeros, redondear=True.
operacion es posicional normal,
*numeros captura el resto
de posicionales,
y redondear viene
después de *args
— lo que lo convierte en un parámetro
que solo se puede pasar
por nombre.
Si escribes
operar("suma", 1, 2, False),
el False iría a
numeros, no a
redondear.
def info(primero, *resto):
print(f"primero: {primero}")
print(f"resto: {resto}")
print(f"total: {len(resto) + 1} argumentos")
info("a")
info("a", "b", "c")
info(1, 2, 3, 4, 5)
operar
hay que pasar
redondear=False
por nombre y no por posición?
¿Qué pasaría si escribieras
operar("promedio", 1, 2, 3, False)?
maximo_de
que reciba cualquier número
de argumentos numéricos
y devuelva el mayor.
Sin usar la función
built-in max().
*args.
¿Qué imprime?
¿Hay algún problema?
def combinar(separador, *palabras):
return separador.join(palabras)
print(combinar("-", "python", "es", "genial"))
print(combinar(" ", "hola", "mundo"))
print(combinar(","))
print(combinar("+", "uno", 2, "tres"))
info("a"):
primero="a",
resto=(),
total: 1 argumentos.
info("a", "b", "c"):
primero="a",
resto=('b', 'c'),
total: 3 argumentos.
info(1, 2, 3, 4, 5):
primero=1,
resto=(2, 3, 4, 5),
total: 5 argumentos.
redondear viene
después de *numeros
en la definición.
Todo lo que va después
de un *
solo puede pasarse por nombre.
Si escribes
operar("promedio",
1, 2, 3, False),
el False va
a numeros
— la tupla sería
(1, 2, 3, False)
— y redondear
usaría su default
True.
El resultado sería incorrecto
porque se intenta hacer
sum((1, 2, 3, False))
— que funciona porque
False vale 0 —
pero no es lo que se quería.
def maximo_de(*args):
if not args:
return None
maximo = args[0]
for n in args:
if n > maximo:
maximo = n
return maximo
print(maximo_de(3, 1, 7, 2)) # 7
print(maximo_de(5)) # 5
print(maximo_de()) # None
combinar("-", "python",
"es", "genial")
→ "python-es-genial".
combinar(" ", "hola",
"mundo")
→ "hola mundo".
combinar(",")
→ ""
— string vacío,
palabras
es tupla vacía.
combinar("+", "uno",
2, "tres")
→ TypeError
— str.join()
requiere que todos
los elementos sean strings,
y 2 es un entero.
Ese es el problema:
la función no valida
que todos los argumentos
en *palabras
sean strings.
**kwargs:
número variable de keyword arguments
teoría
**kwargs es el equivalente
de *args pero para
keyword arguments.
Captura todos los argumentos
nombrados que no tienen parámetro
asignado y los empaqueta
en un diccionario.
Las claves son los nombres
de los argumentos y los valores
son los valores pasados.
Al igual que args,
el nombre kwargs
es solo una convención —
lo que importa son los dos asteriscos.
Dentro de la función,
kwargs es un diccionario
normal que puedes recorrer,
filtrar o consultar.
— El ** tiene dos usos opuestos —
** en la llamada — desempaqueta
d = {"r": 0.1, "n": 5}
f(**d)
# equivale a f(r=0.1, n=5)
# expande el dict en keywords
** en la definición — empaqueta
def f(**kwargs):
print(kwargs)
f(r=0.1, n=5)
# kwargs = {"r": 0.1, "n": 5}
# recoge keywords en dict
— **kwargs básico —
# kwargs es un diccionario dentro de la función
def imprimir_datos(**kwargs):
print(f"kwargs es: {kwargs}")
print(f"tipo: {type(kwargs)}")
imprimir_datos(nombre="Ana", edad=28)
# kwargs = {'nombre': 'Ana', 'edad': 28}
imprimir_datos(x=1, y=2, z=3)
# kwargs = {'x': 1, 'y': 2, 'z': 3}
imprimir_datos()
# kwargs = {} — dict vacío
print("---")
# Acceder a claves específicas
def saludar(**kwargs):
if "nombre" in kwargs:
print(f"Hola, {kwargs['nombre']}!")
else:
print("Hola, desconocido!")
saludar(nombre="Luis")
saludar(nombre="Ana", ciudad="Madrid")
saludar(ciudad="Madrid") # sin nombre
print("---")
# Iterar sobre kwargs como un dict normal
def imprimir_perfil(**kwargs):
for clave, valor in kwargs.items():
print(f" {clave.capitalize()}: {valor}")
imprimir_perfil(nombre="Ana", edad=28,
ciudad="Madrid", rol="analista")
— **kwargs combinado con parámetros normales —
**kwargs debe ir
siempre al final
de todos los parámetros.
Los parámetros antes de
**kwargs reciben
sus argumentos primero —
los keyword arguments restantes
van al diccionario.
# Parámetros obligatorios + kwargs
def imprimir_usuario(nombre, edad, **kwargs):
print(f"Nombre: {nombre}, Edad: {edad}")
if kwargs:
print("Datos adicionales:")
for k, v in kwargs.items():
print(f" {k}: {v}")
imprimir_usuario("Ana", 28)
print("---")
imprimir_usuario("Bob", 35, ciudad="Madrid", rol="analista")
print("---")
# *args y **kwargs juntos — el orden es obligatorio
# def f(normal, *args, **kwargs)
def todo(primero, *args, **kwargs):
print(f"primero: {primero}")
print(f"args: {args}")
print(f"kwargs: {kwargs}")
todo(1, 2, 3, x=10, y=20)
# primero=1, args=(2,3), kwargs={'x':10,'y':20}
— Por qué las librerías usan **kwargs —
Funciones como plt.plot(),
df.to_csv() o
sklearn usan
**kwargs porque
ofrecen decenas de opciones
de personalización.
Definir un parámetro explícito
por cada opción sería inmanejable.
Con **kwargs
el usuario pasa solo las opciones
que necesita — el resto
usa valores por defecto internos.
# Simular cómo plt.plot usa **kwargs
def graficar(x, y, **kwargs):
# kwargs puede contener: color, linewidth,
# linestyle, marker, alpha, label...
color = kwargs.get("color", "blue")
linewidth = kwargs.get("linewidth", 1)
label = kwargs.get("label", "sin etiqueta")
print(f"Graficando '{label}': "
f"color={color}, lw={linewidth}")
# plt.plot(x, y, **kwargs) ← pasa todo a matplotlib
# Cada llamada puede personalizar lo que necesita
graficar([1, 2], [3, 4],
color="red", linewidth=2, label="Ventas")
graficar([1, 2], [3, 4],
color="green", label="Costes")
graficar([1, 2], [3, 4]) # todo por defecto
print("---")
# kwargs.get() — acceso seguro con valor por defecto
# si la clave no existe, devuelve el segundo argumento
def configurar(**kwargs):
verbose = kwargs.get("verbose", False)
max_iter = kwargs.get("max_iter", 100)
tol = kwargs.get("tol", 1e-4)
print(f"verbose={verbose}, max_iter={max_iter}, tol={tol}")
configurar(verbose=True)
configurar(max_iter=500, tol=1e-6)
configurar() # todos por defecto
def analizar(datos, **opciones):
print(f"datos: {datos}")
print(f"opciones: {opciones}")
analizar([1, 2, 3])
analizar([1, 2], metodo="media", redondear=True)
analizar([1], **{"umbral": 0.5, "verbose": False})
**?
¿Cuándo se usa cada uno?
# Uso A
def f(**kwargs):
print(kwargs)
# Uso B
d = {"x": 1, "y": 2}
f(**d)
crear_etiqueta
que reciba un texto obligatorio
y cualquier número de opciones
de estilo como keyword arguments
— color,
tamaño,
negrita.
Si no se pasa una opción,
usa valores por defecto.
*args
y **kwargs.
¿Qué imprime cada llamada?
¿Hay algún problema?
def registrar(*args, **kwargs):
print(f"posicionales: {args}")
print(f"nombrados: {kwargs}")
print(f"total args: {len(args) + len(kwargs)}")
registrar("evento1", "evento2",
usuario="Ana", nivel="info")
registrar()
registrar(usuario="Bob")
registrar("evento", "evento", usuario="Ana",
usuario="Bob")
analizar([1,2,3]):
datos=[1,2,3],
opciones={}.
analizar([1,2],
metodo="media",
redondear=True):
datos=[1,2],
opciones={'metodo':
'media', 'redondear': True}.
analizar([1],
**{"umbral": 0.5,
"verbose": False}):
datos=[1],
opciones={'umbral': 0.5,
'verbose': False}
— el dict se desempaqueta
como keywords antes
de llegar a la función.
Uso A — **kwargs
en la definición:
la función acepta cualquier
número de keyword arguments
y los empaqueta en un dict.
Se usa cuando diseñas
una función flexible.
Uso B — **d
en la llamada:
desempaqueta el diccionario
como keyword arguments
antes de pasarlos.
Se usa cuando tienes
los argumentos en un dict
y quieres pasarlos a una función.
**kwargs en la definición
empaqueta keywords en un dict.
**d en la llamada
desempaqueta un dict en keywords.
El mismo operador,
el mismo símbolo,
significados opuestos
según el contexto.
def crear_etiqueta(texto, **kwargs):
color = kwargs.get("color", "negro")
tamaño = kwargs.get("tamaño", 12)
negrita = kwargs.get("negrita", False)
estilo = "negrita" if negrita else "normal"
return (f"[{texto}] "
f"color={color}, "
f"tamaño={tamaño}, "
f"estilo={estilo}")
print(crear_etiqueta("Título"))
print(crear_etiqueta("Alerta", color="rojo", negrita=True))
print(crear_etiqueta("Nota", tamaño=10, color="gris"))
Primera llamada:
posicionales:
('evento1', 'evento2'),
nombrados:
{'usuario': 'Ana',
'nivel': 'info'},
total args: 4.
Segunda llamada:
posicionales: (),
nombrados: {},
total args: 0.
Tercera llamada:
posicionales: (),
nombrados:
{'usuario': 'Bob'},
total args: 1.
Cuarta llamada:
SyntaxError:
keyword argument repeated:
usuario
— usuario
aparece dos veces
como keyword.
Los diccionarios no permiten
claves duplicadas
y Python detecta este error
antes de ejecutar.
*
y
/
en la definición: forzar posicional o keyword
teoría
Python permite controlar con precisión
cómo deben pasarse los argumentos
a una función — no solo cuántos,
sino si deben ir por posición,
por nombre o cualquiera de los dos.
Para eso existen dos marcadores
especiales en la definición:
/ y *.
Son opcionales — la mayoría de funciones no los necesitan. Pero aparecen con frecuencia en librerías como pandas, numpy y sklearn, y en código generado por IA para funciones complejas. Saber leerlos es esencial para entender exactamente cómo se puede llamar una función.
— * solo: forzar keyword arguments —
Un * solo — sin nombre —
en la definición marca una frontera:
todo lo que viene
después del *
debe pasarse obligatoriamente
por nombre.
No se puede pasar por posición.
Se usa para mejorar la claridad del código — obliga al usuario a ser explícito con ciertos argumentos, eliminando errores de orden silenciosos.
# Sin * — edad y email pueden pasarse por posición
def crear_usuario_v1(nombre, edad, email):
print(f"{nombre}, {edad}, {email}")
crear_usuario_v1("Ana", 28, "ana@mail.com") # funciona
crear_usuario_v1("Ana", "ana@mail.com", 28) # error silencioso
print("---")
# Con * — edad y email deben pasarse por nombre
def crear_usuario_v2(nombre, *, edad, email):
print(f"{nombre}, {edad}, {email}")
# ✓ nombre por posición, edad y email por nombre
crear_usuario_v2("Ana", edad=28, email="ana@mail.com")
# ❌ intentar pasar edad por posición
try:
crear_usuario_v2("Ana", 28, "ana@mail.com")
except TypeError as e:
print(f"TypeError: {e}")
print("---")
# * con default — keyword obligatorio pero opcional
def procesar(datos, *, metodo="media", redondear=True):
print(f"datos={datos}, metodo={metodo}, redondear={redondear}")
procesar([1, 2, 3]) # defaults
procesar([1, 2, 3], metodo="mediana") # un keyword
procesar([1, 2, 3], redondear=False,
metodo="moda") # orden libre
— / solo: forzar argumentos posicionales —
Una / en la definición
marca otra frontera: todo lo que viene
antes de la /
debe pasarse obligatoriamente
por posición.
No se puede pasar por nombre.
Se usa menos frecuentemente — principalmente en funciones built-in de Python y en librerías científicas. Aparece cuando el nombre del parámetro es un detalle de implementación que no debería ser parte de la interfaz pública.
# Con / — nombre y edad deben pasarse por posición
def mostrar(nombre, edad, /, saludo="Hola"):
print(f"{saludo}, {nombre} ({edad} años)")
# ✓ nombre y edad por posición
mostrar("Ana", 28)
mostrar("Ana", 28, saludo="Buenos días")
# ❌ intentar pasar nombre por keyword
try:
mostrar(nombre="Ana", edad=28)
except TypeError as e:
print(f"TypeError: {e}")
print("---")
# / aparece en funciones built-in de Python
# Por ejemplo, len() solo acepta posicional
try:
len(obj=[1, 2, 3]) # TypeError
except TypeError as e:
print(f"TypeError: {e}")
— La firma completa: /, pos_o_kwd, *, solo_kwd, **kwargs —
Python permite combinar todos estos
mecanismos en una sola función.
La regla es que deben aparecer
en este orden exacto:
parámetros antes de /,
luego parámetros flexibles,
luego * o *args,
luego parámetros solo-keyword,
luego **kwargs.
# La firma más completa posible
def funcion_completa(pos1, pos2, /, flexible, *, solo_kwd, **kwargs):
print(f"pos1={pos1}, pos2={pos2}") # solo posición
print(f"flexible={flexible}") # posición o keyword
print(f"solo_kwd={solo_kwd}") # solo keyword
print(f"kwargs={kwargs}") # keywords extra
funcion_completa(1, 2, 3, solo_kwd=4)
funcion_completa(1, 2, flexible=3, solo_kwd=4)
funcion_completa(1, 2, 3, solo_kwd=4, extra=5)
print("---")
# Ejemplo real: una función de análisis bien diseñada
def analizar_serie(serie, /, *,
metodo="media",
redondear=True,
**opciones):
print(f"serie: {serie}")
print(f"metodo: {metodo}, redondear: {redondear}")
if opciones:
print(f"opciones extra: {opciones}")
analizar_serie([1, 2, 3])
analizar_serie([1, 2, 3], metodo="mediana")
analizar_serie([1, 2, 3], metodo="media",
umbral=0.5, verbose=True)
— Tabla resumen: todos los tipos de parámetros —
| Sección | Significado | Cómo se pasa |
|---|---|---|
pos1, pos2, /
|
Antes de / |
Solo por posición |
flexible
|
Entre / y * |
Posición o keyword |
*args
|
Captura posicionales extra | Solo por posición → tupla |
*, solo_kwd
|
Después de * |
Solo por keyword |
**kwargs
|
Captura keywords extra | Solo por keyword → dict |
def modelo(X, y, /, test_size=0.2, *,
random_state=42, verbose=False):
pass
¿Cuáles son solo posicionales?
¿Cuáles son solo keyword?
¿Cuáles son flexibles?
def f(a, b, /, c, *, d):
print(a, b, c, d)
f(1, 2, 3, d=4)
f(1, 2, c=3, d=4)
f(a=1, b=2, c=3, d=4)
f(1, 2, 3, 4)
len()
no acepta argumentos por nombre?
¿Qué ventaja tiene eso
para una función tan básica?
def analizar(df, columna, /, *,
metodo="media",
umbral=None,
**opciones):
pass
# ¿Cuáles de estas llamadas son válidas?
analizar(df, "ventas")
analizar(df, "ventas", metodo="mediana")
analizar(df, columna="ventas")
analizar(df, "ventas", "media")
analizar(df, "ventas", verbose=True)
X y y
están antes de /
→ solo posicionales — obligatorios.
test_size
está entre /
y *
→ flexible — posición o keyword,
tiene default por lo que es opcional.
random_state
y verbose
están después de *
→ solo keyword — opcionales
con default.
X e y
siempre posicionales,
los hiperparámetros
siempre por nombre.
f(1, 2, 3, d=4)
→ válida:
a=1,
b=2
por posición,
c=3 flexible,
d=4 keyword.
f(1, 2, c=3, d=4)
→ válida:
a=1,
b=2
por posición,
c=3
y d=4
por keyword.
f(a=1, b=2, c=3, d=4)
→ TypeError:
a y b
están antes de /
— solo pueden ir
por posición.
f(1, 2, 3, 4)
→ TypeError:
d está después
de *
— solo puede ir por keyword,
no por posición.
len() no acepta
argumentos por nombre porque
su parámetro interno se llama
obj en CPython —
un detalle de implementación
que no es parte de la interfaz.
Si se pudiera llamar con
len(obj=[1,2,3]),
cambiar el nombre interno
en una nueva versión de Python
rompería todo el código
que lo usara así.
Forzarlo posicional protege
la implementación interna.
df y
columna
antes de /
→ solo posicionales,
obligatorios.
metodo y
umbral
después de *
→ solo keyword, opcionales.
**opciones
→ cualquier keyword extra.
Llamadas válidas:
analizar(df, "ventas")
✓,
analizar(df, "ventas",
metodo="mediana") ✓,
analizar(df, "ventas",
verbose=True) ✓
— verbose
va a **opciones.
Llamadas inválidas:
analizar(df,
columna="ventas")
→ TypeError
— columna
es solo posicional.
analizar(df, "ventas",
"media")
→ TypeError
— "media"
es posicional pero no hay
tercer parámetro posicional.