20. Parámetros y argumentos de funciones
En el Capítulo 19, aprendimos a definir y llamar funciones (functions) con parámetros básicos. Ahora exploraremos en profundidad el sistema flexible de parámetros y argumentos de Python. Comprender estos mecanismos te permite escribir funciones que sean a la vez potentes y fáciles de usar.
20.1) Argumentos posicionales y de palabra clave
Cuando llamas a una función, puedes pasar argumentos de dos formas fundamentales: por posición o por nombre (palabra clave).
20.1.1) Argumentos posicionales
Los argumentos posicionales se emparejan con los parámetros según su orden. El primer argumento va al primer parámetro, el segundo al segundo parámetro, y así sucesivamente.
def calculate_discount(price, discount_percent):
"""Calculate the final price after applying a discount."""
discount_amount = price * (discount_percent / 100)
final_price = price - discount_amount
return final_price
# Pasar argumentos por posición
result = calculate_discount(100, 20)
print(result)Output:
80.0En este ejemplo, 100 se asigna a price y 20 a discount_percent basándose únicamente en sus posiciones en la llamada a la función.
El orden importa de forma crítica con los argumentos posicionales:
# Ejemplo: queremos calcular un artículo de $100 con un 20% de descuento
# Orden correcto: primero price, luego discount
print(calculate_discount(100, 20))
# Orden incorrecto: primero discount, luego price
print(calculate_discount(20, 100))Output:
80.0
-16.0Cuando intercambias los argumentos, Python no sabe que cometiste un error: simplemente los asigna en orden. Esto produce un resultado matemáticamente válido, pero lógicamente incorrecto (¡un precio negativo!).
20.1.2) Argumentos de palabra clave
Los argumentos de palabra clave especifican explícitamente qué parámetro recibe qué valor usando el nombre del parámetro seguido de un signo igual y el valor. Esto hace que tu código sea más legible y te protege de errores de orden.
def create_user_profile(username, email, age):
"""Create a user profile with the given information."""
profile = f"User: {username}\nEmail: {email}\nAge: {age}"
return profile
# Usar argumentos de palabra clave
profile = create_user_profile(username="alice_smith", email="alice@example.com", age=28)
print(profile)Output:
User: alice_smith
Email: alice@example.com
Age: 28Con argumentos de palabra clave, el orden no importa:
# Mismo resultado, distinto orden
profile1 = create_user_profile(username="bob", email="bob@example.com", age=35)
profile2 = create_user_profile(age=35, username="bob", email="bob@example.com")
profile3 = create_user_profile(email="bob@example.com", age=35, username="bob")
# Los tres producen resultados idénticos
print(profile1 == profile2 == profile3)Output:
TrueEsta flexibilidad es especialmente valiosa cuando una función tiene muchos parámetros, lo que facilita ver qué valor corresponde a qué parámetro.
20.1.3) Mezclar argumentos posicionales y de palabra clave
Puedes combinar ambos estilos en una sola llamada a la función, pero hay una regla importante: los argumentos posicionales deben ir antes que los argumentos de palabra clave.
def format_address(street, city, state, zip_code):
"""Format a mailing address."""
return f"{street}\n{city}, {state} {zip_code}"
# Válido: primero argumentos posicionales, luego argumentos de palabra clave
address = format_address("123 Main St", "Springfield", state="IL", zip_code="62701")
print(address)Output:
123 Main St
Springfield, IL 62701Aquí, "123 Main St" y "Springfield" son posicionales (se asignan a street y city), mientras que state y zip_code se especifican por nombre.
Intentar colocar argumentos posicionales después de argumentos de palabra clave provoca un error:
# Inválido: argumento posicional después de argumento de palabra clave
# address = format_address(street="123 Main St", "Springfield", state="IL", zip_code="62701")
# SyntaxError: positional argument follows keyword argumentPython impone esta regla porque, una vez que empiezas a usar argumentos de palabra clave, se vuelve ambiguo qué parámetro posicional debería rellenar un argumento posterior sin nombre.
20.1.4) Cuándo usar cada estilo
Usa argumentos posicionales cuando:
- La función tiene pocos parámetros (normalmente 1-3)
- El orden de los parámetros es obvio e intuitivo
- La función se usa comúnmente y el orden es bien conocido
# Obvio y conciso
print(len("hello"))
result = max(10, 20, 5)Usa argumentos de palabra clave cuando:
- La función tiene muchos parámetros
- Los significados de los parámetros no son inmediatamente obvios
- Quieres omitir algunos parámetros que tienen valores por defecto (lo veremos a continuación)
- Quieres que tu código se autodocumente
# Claro y explícito
user = create_user_profile(username="charlie", email="charlie@example.com", age=42)20.2) Valores por defecto de parámetros
Las funciones pueden especificar valores por defecto para los parámetros. Cuando quien llama no proporciona un argumento para un parámetro con valor por defecto, Python usa el valor por defecto en su lugar.
20.2.1) Definir parámetros con valores por defecto
Los valores por defecto se especifican en la definición de la función usando el operador de asignación:
def greet_user(name, greeting="Hello"):
"""Greet a user with a customizable greeting."""
return f"{greeting}, {name}!"
# Usar el saludo por defecto
print(greet_user("Alice"))
# Proporcionar un saludo personalizado
print(greet_user("Bob", "Good morning"))
print(greet_user("Carol", greeting="Hi"))Output:
Hello, Alice!
Good morning, Bob!
Hi, Carol!El parámetro greeting tiene un valor por defecto "Hello". Cuando llamas a greet_user("Alice"), Python usa este valor por defecto. Cuando proporcionas un segundo argumento, reemplaza el valor por defecto.
20.2.2) Los parámetros con valores por defecto deben ir después de los parámetros obligatorios
Python requiere que los parámetros con valores por defecto aparezcan después de todos los parámetros sin valores por defecto. Esta regla evita la ambigüedad sobre qué argumentos corresponden a qué parámetros.
# Correcto: primero parámetros obligatorios, luego valores por defecto
def create_product(name, price, category="General", in_stock=True):
"""Create a product record."""
return {
"name": name,
"price": price,
"category": category,
"in_stock": in_stock
}
product = create_product("Laptop", 999.99)
print(product)Output:
{'name': 'Laptop', 'price': 999.99, 'category': 'General', 'in_stock': True}Intentar colocar un parámetro obligatorio después de uno con valor por defecto provoca un error de sintaxis:
# Invalid: required parameter after default parameter
# def invalid_function(name="Unknown", age):
# return f"{name} is {age} years old"
# SyntaxError: non-default argument follows default argumentEsto tiene sentido: si name tiene un valor por defecto pero age no, ¿cómo sabría Python si invalid_function(25) significa name=25 con age faltando, o age=25 con name usando su valor por defecto? La regla elimina esta ambigüedad.
20.2.3) Usos prácticos de los parámetros por defecto
Los parámetros por defecto son muy útiles en funciones donde ciertos argumentos rara vez cambian:
def calculate_shipping(weight, distance, express=False):
"""Calculate shipping cost based on weight and distance."""
base_rate = 0.50 * weight + 0.10 * distance
if express:
base_rate *= 2 # Los envíos express cuestan el doble
return round(base_rate, 2)
# La mayoría de los envíos son estándar
standard_cost = calculate_shipping(5, 100)
print(f"Standard: ${standard_cost}")
# Ocasionalmente alguien necesita express
express_cost = calculate_shipping(5, 100, express=True)
print(f"Express: ${express_cost}")Output:
Standard: $12.5
Express: $25.0Este diseño hace que el caso común (envío estándar) sea sencillo de usar, y aun así soporta el caso menos común (envío express) cuando se necesita.
20.2.4) Múltiples valores por defecto y sobreescritura selectiva
Cuando una función tiene múltiples parámetros con valores por defecto, puedes sobrescribir cualquier combinación de ellos usando argumentos de palabra clave:
def format_currency(amount, currency="USD", show_symbol=True, decimal_places=2):
"""Format a number as currency."""
symbols = {"USD": "$", "EUR": "€", "GBP": "£", "JPY": "¥"}
formatted = f"{amount:.{decimal_places}f}"
if show_symbol and currency in symbols:
formatted = f"{symbols[currency]}{formatted}"
return formatted
# Usar todos los valores por defecto
print(format_currency(42.5))
# Sobrescribir solo currency
print(format_currency(42.5, currency="EUR"))
# Sobrescribir múltiples valores por defecto
print(format_currency(42.5, currency="JPY", decimal_places=0))Output:
$42.50
€42.50
¥42Esta flexibilidad permite que quien llama personalice exactamente lo que necesita, manteniendo la llamada a la función concisa.
20.3) Listas de argumentos de longitud variable con *args
A veces quieres que una función acepte cualquier número de argumentos sin saber de antemano cuántos habrá. Python proporciona *args para este propósito.
20.3.1) Comprender *args
La sintaxis *args en una lista de parámetros recopila todos los argumentos posicionales extra en una tupla. El nombre args es una convención (abreviatura de "arguments"), pero puedes usar cualquier nombre de parámetro válido después del asterisco.
def calculate_total(*numbers):
"""Calculate the sum of any number of values."""
total = 0
for num in numbers:
total += num
return total
# Funciona con cualquier número de argumentos
print(calculate_total(10))
print(calculate_total(10, 20))
print(calculate_total(10, 20, 30, 40))
print(calculate_total())Output:
10
30
100
0Dentro de la función, numbers es una tupla que contiene todos los argumentos posicionales pasados a la función. Cuando no se proporcionan argumentos, es una tupla vacía.
20.3.2) Combinar parámetros normales con *args
Puedes tener parámetros normales antes de *args. Los parámetros normales consumen los primeros argumentos, y *args recopila el resto:
def create_team(team_name, *members):
"""Create a team with a name and any number of members."""
member_list = ", ".join(members)
return f"Team {team_name}: {member_list}"
# El primer argumento va a team_name, el resto va a members
print(create_team("Alpha", "Alice", "Bob"))
print(create_team("Beta", "Carol"))
print(create_team("Gamma", "Dave", "Eve", "Frank", "Grace"))Output:
Team Alpha: Alice, Bob
Team Beta: Carol
Team Gamma: Dave, Eve, Frank, GraceEl primer argumento ("Alpha", "Beta" o "Gamma") se asigna a team_name, y todos los argumentos restantes se recopilan en la tupla members.
20.4) Parámetros de solo palabra clave y parámetros **kwargs
Python proporciona dos mecanismos adicionales para manejar argumentos: los parámetros de solo palabra clave y **kwargs para recopilar argumentos arbitrarios de palabra clave.
20.4.1) Parámetros de solo palabra clave
Los parámetros de solo palabra clave deben especificarse usando argumentos de palabra clave: no se pueden pasar de forma posicional. Los creas colocándolos después de un * o después de *args en la lista de parámetros.
def create_account(username, *, email, age):
"""Create an account. Email and age must be specified by name."""
return {
"username": username,
"email": email,
"age": age
}
# Correcto: email y age especificados por palabra clave
account = create_account("alice", email="alice@example.com", age=28)
print(account)
# Invalid: trying to pass email and age positionally
# account = create_account("bob", "bob@example.com", 30)
# TypeError: create_account() takes 1 positional argument but 3 were givenOutput:
{'username': 'alice', 'email': 'alice@example.com', 'age': 28}El * en la lista de parámetros actúa como separador. Todo lo que esté después debe pasarse como argumento de palabra clave. Esto es útil cuando quieres obligar a quien llama a ser explícito sobre ciertos parámetros, haciendo el código más legible y menos propenso a errores.
También puedes combinar parámetros normales, *args y parámetros de solo palabra clave:
def log_event(event_type, *details, severity="INFO", timestamp=None):
"""Log an event with optional details and metadata."""
# Aprenderemos sobre el módulo datetime en detalle en el Capítulo 39,
# pero por ahora, solo debes saber que estas líneas obtienen la hora actual
# y la formatean como una cadena de marca de tiempo
from datetime import datetime
if timestamp is None:
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
details_str = " | ".join(details)
return f"[{timestamp}] {severity}: {event_type} - {details_str}"
# event_type es posicional, details se recopila con *details,
# severity y timestamp son de solo palabra clave
print(log_event("Login", "User: alice", "IP: 192.168.1.1"))
print(log_event("Error", "Database connection failed", severity="ERROR"))Output (timestamp will vary based on when you run the code):
[2025-12-18 19:29:16] INFO: Login - User: alice | IP: 192.168.1.1
[2025-12-18 19:29:16] ERROR: Error - Database connection failed20.4.2) Comprender **kwargs
La sintaxis **kwargs recopila todos los argumentos extra de palabra clave en un diccionario. Al igual que args, el nombre kwargs es convencional (abreviatura de "keyword arguments"), pero puedes usar cualquier nombre válido después del doble asterisco.
def create_product(**attributes):
"""Create a product with any number of attributes."""
product = {}
for key, value in attributes.items():
product[key] = value
return product
# Pasa cualquier argumento de palabra clave que quieras
laptop = create_product(name="Laptop", price=999.99, brand="TechCorp", in_stock=True)
print(laptop)
phone = create_product(name="Phone", price=699.99, color="Black")
print(phone)Output:
{'name': 'Laptop', 'price': 999.99, 'brand': 'TechCorp', 'in_stock': True}
{'name': 'Phone', 'price': 699.99, 'color': 'Black'}Dentro de la función, attributes es un diccionario donde las claves son los nombres de los parámetros y los valores son los argumentos pasados.
20.4.3) Combinar parámetros normales, *args y **kwargs
Puedes usar todos estos mecanismos juntos, pero deben aparecer en un orden específico:
- Parámetros posicionales normales
*args(si está presente)- Parámetros de solo palabra clave (si están presentes)
**kwargs(si está presente)
def complex_function(required, *args, keyword_only, **kwargs):
"""Demonstrate all parameter types together."""
print(f"Required: {required}")
print(f"Args: {args}")
print(f"Keyword-only: {keyword_only}")
print(f"Kwargs: {kwargs}")
complex_function(
"value1", # required
"value2", "value3", # args
keyword_only="kw", # keyword_only
extra1="e1", # kwargs
extra2="e2" # kwargs
)Output:
Required: value1
Args: ('value2', 'value3')
Keyword-only: kw
Kwargs: {'extra1': 'e1', 'extra2': 'e2'}Esta flexibilidad es muy potente, pero conviene usarla con criterio. La mayoría de las funciones no necesitan todos estos mecanismos.
20.4.4) Caso de uso práctico: funciones de configuración
Un uso común de **kwargs es crear funciones que acepten opciones de configuración:
def connect_to_database(host, port, **options):
"""Connect to a database with flexible configuration options."""
connection_string = f"Connecting to {host}:{port}"
# Procesar cualquier opción adicional
if options.get("ssl"):
connection_string += " with SSL"
if options.get("timeout"):
connection_string += f" (timeout: {options['timeout']}s)"
if options.get("pool_size"):
connection_string += f" (pool size: {options['pool_size']})"
return connection_string
# Conexión básica
print(connect_to_database("localhost", 5432))
# Con SSL
print(connect_to_database("db.example.com", 5432, ssl=True))
# Con múltiples opciones
print(connect_to_database("db.example.com", 5432, ssl=True, timeout=30, pool_size=10))Output:
Connecting to localhost:5432
Connecting to db.example.com:5432 with SSL
Connecting to db.example.com:5432 with SSL (timeout: 30s) (pool size: 10)Este patrón permite que la función acepte cualquier número de parámetros opcionales de configuración sin definirlos todos explícitamente en la lista de parámetros.
20.5) Desempaquetado de argumentos al llamar funciones
Igual que *args y **kwargs recopilan argumentos al definir funciones, puedes usar * y ** para desempaquetar (unpack) colecciones al llamar funciones.
20.5.1) Desempaquetar secuencias con *
El operador * desempaqueta una secuencia (lista, tupla, etc.) en argumentos posicionales separados:
def calculate_rectangle_area(width, height):
"""Calculate the area of a rectangle."""
return width * height
# En lugar de pasar los argumentos individualmente
dimensions = [5, 10]
area = calculate_rectangle_area(dimensions[0], dimensions[1])
print(area)
# Desempaquetar la lista directamente
area = calculate_rectangle_area(*dimensions)
print(area)Output:
50
50Cuando escribes *dimensions, Python desempaqueta la lista [5, 10] en dos argumentos separados, como si hubieras escrito calculate_rectangle_area(5, 10).
Esto funciona con cualquier iterable:
def format_name(first, middle, last):
"""Format a full name."""
return f"{first} {middle} {last}"
# Desempaquetar una tupla
name_tuple = ("John", "Q", "Public")
print(format_name(*name_tuple))
# Desempaquetar una lista
name_list = ["Jane", "M", "Doe"]
print(format_name(*name_list))
# Incluso desempaquetar una cadena (cada carácter se convierte en un argumento)
# Esto solo funciona si la función espera el número correcto de argumentos
def show_first_three(a, b, c):
return f"{a}, {b}, {c}"
print(show_first_three(*"ABC"))Output:
John Q Public
Jane M Doe
A, B, C20.5.2) Desempaquetar diccionarios con **
El operador ** desempaqueta un diccionario en argumentos de palabra clave:
def create_user(username, email, age):
"""Create a user profile."""
return f"User: {username}, Email: {email}, Age: {age}"
# Diccionario con claves que coinciden con los nombres de los parámetros
user_data = {
"username": "alice",
"email": "alice@example.com",
"age": 28
}
# Desempaquetar el diccionario
profile = create_user(**user_data)
print(profile)Output:
User: alice, Email: alice@example.com, Age: 28Cuando escribes **user_data, Python desempaqueta el diccionario en argumentos de palabra clave, equivalente a:
create_user(username="alice", email="alice@example.com", age=28)Las claves del diccionario deben coincidir con los nombres de los parámetros de la función, o obtendrás un error:
# Invalid: dictionary key doesn't match parameter name
invalid_data = {"name": "bob", "email": "bob@example.com", "age": 30}
# profile = create_user(**invalid_data)
# TypeError: create_user() got an unexpected keyword argument 'name'20.5.3) Combinar desempaquetado con argumentos normales
Puedes mezclar argumentos desempaquetados con argumentos normales:
def calculate_total(base_price, tax_rate, discount):
"""Calculate total price after tax and discount."""
subtotal = base_price * (1 + tax_rate)
total = subtotal * (1 - discount)
return round(total, 2)
# Algunos argumentos normales, otros desempaquetados
pricing = [0.08, 0.10] # tax_rate and discount
total = calculate_total(100, *pricing)
print(total)Output:
97.2También puedes desempaquetar múltiples colecciones en una sola llamada:
def create_full_address(street, city, state, zip_code, country):
"""Create a complete address."""
return f"{street}, {city}, {state} {zip_code}, {country}"
street_address = ["123 Main St", "Springfield"]
location_details = ["IL", "62701", "USA"]
address = create_full_address(*street_address, *location_details)
print(address)Output:
123 Main St, Springfield, IL 62701, USA20.5.4) Ejemplo práctico: llamadas a funciones flexibles
El desempaquetado es particularmente útil cuando trabajas con datos de fuentes externas:
def send_email(recipient, subject, body, cc=None, bcc=None):
"""Send an email with optional CC and BCC."""
message = f"To: {recipient}\nSubject: {subject}\n\n{body}"
if cc:
message += f"\nCC: {cc}"
if bcc:
message += f"\nBCC: {bcc}"
return message
# Datos de email desde un archivo de configuración o una base de datos
email_config = {
"recipient": "user@example.com",
"subject": "Welcome",
"body": "Thank you for signing up!",
"cc": "manager@example.com"
}
# Desempaquetar la configuración directamente
result = send_email(**email_config)
print(result)Output:
To: user@example.com
Subject: Welcome
Thank you for signing up!
CC: manager@example.comEste patrón facilita pasar argumentos de función como estructuras de datos, lo cual es común al construir APIs o procesar archivos de configuración.
20.6) La trampa de los argumentos por defecto mutables (por qué persisten los valores por defecto de listas)
Uno de los escollos más notorios de Python implica usar objetos mutables (como listas o diccionarios) como valores por defecto de parámetros. Comprender este problema es crucial para escribir funciones correctas.
20.6.1) El problema: valores por defecto mutables compartidos
Considera esta función aparentemente inocente:
def add_student(name, grades=[]):
"""Add a student with their grades."""
grades.append(name)
return grades
# Primera llamada
students1 = add_student("Alice")
print(students1)
# Segunda llamada: esperando una lista nueva
students2 = add_student("Bob")
print(students2)
# Tercera llamada
students3 = add_student("Carol")
print(students3)Output:
['Alice']
['Alice', 'Bob']
['Alice', 'Bob', 'Carol']Este comportamiento sorprende a muchos programadores. Cada llamada a add_student() sin proporcionar un argumento grades usa el mismo objeto lista, no uno nuevo. La lista persiste a través de las llamadas a la función, acumulando valores.
20.6.2) Por qué ocurre esto: los valores por defecto se crean una sola vez
La clave para comprender este comportamiento es saber cuándo se crean los valores por defecto. Python evalúa los valores por defecto de parámetros una vez, cuando se define la función, no cada vez que se llama la función.
def demonstrate_default_creation():
"""Show when defaults are created."""
print("Function defined!")
def use_default(value=demonstrate_default_creation()):
"""Use a default that calls a function."""
return value
# El mensaje se imprime cuando la función se DEFINE, no cuando se llamaOutput:
Function defined!Cuando Python encuentra la línea def use_default, evalúa el parámetro por defecto value=demonstrate_default_creation(). Esto llama a demonstrate_default_creation(), que imprime "Function defined!" de inmediato. Las llamadas posteriores a use_default() no vuelven a evaluar el valor por defecto, así que no se imprime nada adicional.
Cuando Python encuentra def add_student(name, grades=[]):, crea un objeto lista vacío y lo guarda como el valor por defecto para grades. Cada llamada posterior que no proporcione un argumento grades usa ese mismo objeto lista.
Aquí tienes una demostración más clara usando identidad de objeto:
def show_list_identity(items=[]):
"""Show that the same list object is reused."""
print(f"List ID: {id(items)}")
items.append("item")
return items
# Cada llamada usa el mismo objeto lista (mismo ID)
show_list_identity()
show_list_identity()
show_list_identity()Output:
List ID: 140234567890123
List ID: 140234567890123
List ID: 140234567890123Los números de ID exactos variarán en tu sistema, pero observa que las tres llamadas muestran el mismo valor de ID, lo que prueba que están usando el mismo objeto lista. La función id() devuelve un identificador único para cada objeto en memoria: cuando los IDs coinciden, es el mismo objeto.
20.6.3) El patrón correcto: usar None como valor por defecto
La solución estándar es usar None como valor por defecto y crear un objeto mutable nuevo dentro de la función:
def add_student_correct(name, grades=None):
"""Add a student with their grades (correct version)."""
if grades is None:
grades = [] # Crear una lista NUEVA cada vez
grades.append(name)
return grades
# Ahora cada llamada obtiene su propia lista
students1 = add_student_correct("Alice")
print(students1)
students2 = add_student_correct("Bob")
print(students2)
students3 = add_student_correct("Carol")
print(students3)Output:
['Alice']
['Bob']
['Carol']Este patrón funciona porque None es inmutable y se crea una lista nueva dentro del cuerpo de la función cada vez que grades es None.
20.6.4) El mismo problema con diccionarios
Este problema afecta a todos los tipos mutables, no solo a las listas:
# WRONG: Dictionary default
def create_config_wrong(key, value, config={}):
"""Create a configuration (BUGGY VERSION)."""
config[key] = value
return config
config1 = create_config_wrong("theme", "dark")
print(config1)
config2 = create_config_wrong("language", "en")
print(config2)
print("---")
# CORRECT: None as default
def create_config_correct(key, value, config=None):
"""Create a configuration (CORRECT VERSION)."""
if config is None:
config = {}
config[key] = value
return config
config1 = create_config_correct("theme", "dark")
print(config1)
config2 = create_config_correct("language", "en")
print(config2)Output:
{'theme': 'dark'}
{'theme': 'dark', 'language': 'en'}
---
{'theme': 'dark'}
{'language': 'en'}20.6.5) Resumen: la regla de oro
Nunca uses objetos mutables (listas, diccionarios, conjuntos) como valores por defecto de parámetros. Usa siempre None y crea el objeto mutable dentro de la función:
# ❌ WRONG
def function(items=[]):
pass
# ✅ CORRECT
def function(items=None):
if items is None:
items = []
# Ahora usa items de forma seguraEste patrón garantiza que cada llamada a la función tenga su propio objeto mutable independiente, evitando bugs misteriosos donde los datos se “filtran” entre llamadas.
En este capítulo, hemos explorado en profundidad el sistema flexible de parámetros y argumentos de Python. Has aprendido cómo usar argumentos posicionales y de palabra clave, proporcionar valores por defecto, manejar números variables de argumentos con *args y **kwargs, desempaquetar colecciones al llamar funciones y evitar la trampa de los argumentos por defecto mutables.
Estos mecanismos te dan herramientas potentes para diseñar interfaces de funciones que sean tanto flexibles como fáciles de usar. A medida que escribas más funciones, desarrollarás intuición sobre qué patrones de parámetros se adaptan mejor a diferentes situaciones. La clave es equilibrar flexibilidad con claridad: haz que tus funciones sean fáciles de llamar correctamente y difíciles de llamar incorrectamente.