En Python, una función no es una construcción especial del lenguaje que solo existe para ser invocada — es un objeto como cualquier otro. Un objeto con tipo (function), con atributos, que vive en el heap y cuya referencia puedes pasar, almacenar, o retornar igual que harías con un entero o un string. Eso es exactamente lo que significa primera clase (first-class citizen): el lenguaje no te pone restricciones sobre qué puedes hacer con ese valor.
La razón de que esto funcione así es profundamente pythónica: def no es magia compiladora, es una sentencia que se ejecuta en tiempo de ejecución y produce un objeto de tipo function, asignándolo al nombre que le diste. Cuando escribes def procesar(x): ..., Python está ejecutando algo conceptualmente equivalente a procesar = function(...). El nombre es solo una referencia; el objeto puede tener muchas.
Esto importa en cuanto empiezas a diseñar código flexible. Pasar key=str.lower a sorted() es primera clase. Registrar handlers en un diccionario por nombre de evento es primera clase. Un decorador que envuelve comportamiento es primera clase. Si no tienes claro el modelo, tarde o temprano escribirás código que podría ser una línea elegante pero termina siendo diez líneas rígidas.
Lo que se rompe cuando no lo entiendes: tiendes a sobre-usar herencia para variar comportamiento, o a sembrar if/elif por todas partes donde un simple diccionario de funciones resolvería el problema limpiamente. El otro error clásico es confundir f (la referencia, el objeto) con f() (el resultado de llamarla) — los pasas donde no debes y el error explota lejos del origen.
from typing import Callable, Any
import inspect
# ── 1. Introspección: las funciones tienen atributos propios ──────────────
def greet(name: str, greeting: str = "Hola") -> str:
"""Devuelve un saludo personalizado."""
return f"{greeting}, {name}!"
print(greet.__name__) # 'greet'
print(greet.__doc__) # 'Devuelve un saludo personalizado.'
print(greet.__defaults__) # ('Hola',) — valores por defecto posicionales
print(greet.__annotations__) # {'name': <class 'str'>, 'greeting': <class 'str'>, 'return': <class 'str'>}
# ── 2. Pasar funciones como argumentos ───────────────────────────────────
records = ["Alice", "bob", "CAROL", "dave"]
# str.lower es un método no ligado — también es un objeto callable
sorted_records = sorted(records, key=str.lower)
print(sorted_records) # ['Alice', 'bob', 'CAROL', 'dave']
# map() acepta cualquier callable como primer argumento
lengths = list(map(len, records))
print(lengths) # [5, 3, 5, 4]
# ── 3. Almacenar funciones en estructuras de datos ───────────────────────
def add(a: float, b: float) -> float:
return a + b
def subtract(a: float, b: float) -> float:
return a - b
def multiply(a: float, b: float) -> float:
return a * b
# Un dict como tabla de despacho — más extensible que cualquier if/elif
OPERATIONS: dict[str, Callable[[float, float], float]] = {
"add": add,
"subtract": subtract,
"multiply": multiply,
}
def calculate(op: str, x: float, y: float) -> float:
if op not in OPERATIONS:
raise ValueError(f"Operación desconocida: {op!r}")
return OPERATIONS[op](x, y) # llamamos el objeto que está en el dict
print(calculate("multiply", 6, 7)) # 42.0
# ── 4. Retornar funciones: factories y closures ──────────────────────────
def make_multiplier(factor: float) -> Callable[[float], float]:
# 'factor' queda capturado en el closure de la función interna
def multiplier(x: float) -> float:
return x * factor
return multiplier # retornamos el objeto función, no lo llamamos
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(10)) # 20.0
print(triple(10)) # 30.0
# Cada closure tiene su propia celda con el valor de 'factor'
print(double.__closure__[0].cell_contents) # 2 (no hagas esto en prod, es útil para debug)
# ── 5. callable() y funciones en atributos de clase ─────────────────────
class Pipeline:
"""Ejecuta una secuencia de transformaciones sobre un valor."""
def __init__(self, *steps: Callable[[Any], Any]):
# Guardamos referencias a funciones como atributo de instancia
self.steps = list(steps)
def add_step(self, fn: Callable[[Any], Any]) -> "Pipeline":
if not callable(fn): # callable() verifica __call__; cubre funciones, métodos, clases, lambdas
raise TypeError(f"{fn!r} no es callable")
self.steps.append(fn)
return self # retornamos self para encadenamiento fluido
def run(self, value: Any) -> Any:
for step in self.steps:
value = step(value)
return value
def strip_spaces(text: str) -> str:
return text.strip()
def to_upper(text: str) -> str:
return text.upper()
pipeline = (
Pipeline(strip_spaces)
.add_step(to_upper)
.add_step(lambda s: s + "!")
)
print(pipeline.run(" hello world ")) # 'HELLO WORLD!'
# ── 6. Sistema de plugins mínimo usando un registro de funciones ─────────
HANDLERS: dict[str, Callable] = {}
def register(event: str) -> Callable:
"""Decorador que registra una función como handler de un evento."""
def decorator(fn: Callable) -> Callable:
HANDLERS[event] = fn
return fn # devolvemos la función sin modificarla — solo la registramos
return decorator
@register("user.created")
def on_user_created(payload: dict) -> None:
print(f"Nuevo usuario: {payload['name']}")
@register("user.deleted")
def on_user_deleted(payload: dict) -> None:
print(f"Usuario eliminado: {payload['id']}")
def dispatch(event: str, payload: dict) -> None:
handler = HANDLERS.get(event)
if handler is None:
raise KeyError(f"Sin handler para {event!r}")
handler(payload)
dispatch("user.created", {"name": "Alice"}) # Nuevo usuario: Alice
dispatch("user.deleted", {"id": 42}) # Usuario eliminado: 42
Qué está pasando aquí y por qué cada decisión importa
El bloque de introspección del principio no es curiosidad académica. __defaults__ y __annotations__ son los mismos atributos que usa inspect.signature() para construir la firma que ves en los IDEs y que frameworks como FastAPI leen para generar validación automática. Entender que son atributos normales del objeto te explica por qué los decoradores que olvidan usar functools.wraps rompen las herramientas de introspección.
La tabla de despacho con OPERATIONS es el patrón que reemplaza al if/elif extenso. Fíjate en la anotación de tipo: Callable[[float, float], float] documenta exactamente el contrato que cada valor del diccionario debe cumplir. Agregar una nueva operación no requiere tocar calculate() — solo añades una entrada al diccionario. Eso es el principio abierto/cerrado sin ninguna herencia involucrada.
En make_multiplier, cada llamada a la factory produce un objeto función distinto con su propia captura de factor. El closure no comparte estado entre instancias — double y triple son independientes. Acceder a __closure__[0].cell_contents es algo que solo haces mientras depuras: te confirma que el valor está efectivamente capturado, no evaluado perezosamente como en el error clásico de closures en loops.
callable() en Pipeline.add_step() es más robusto que isinstance(fn, types.FunctionType) porque acepta cualquier objeto con __call__: lambdas, clases, instancias de clases con __call__ implementado. Es la forma pythónica de verificar “¿puedo invocar esto?”.
El sistema de plugins con @register demuestra el patrón real detrás de decoradores como los de Flask o Click. register("user.created") es una llamada que retorna decorator. Luego Python aplica decorator a la función definida a continuación. El efecto neto: la función queda registrada en HANDLERS sin que el código que la define sepa nada del mecanismo de despacho — acoplamiento cero.
Errores que debes conocer
Error: Pasar el resultado de llamar una función en lugar de la función misma cuando registras un callback o key.
# ❌ Wrong records = ["Alice", "bob", "CAROL"] sorted_records = sorted(records, key=str.lower()) # TypeError: lower() toma exactamente 1 argumento # ✅ Right sorted_records = sorted(records, key=str.lower)
str.lower() sin argumento falla porque es un método no ligado que necesita una instancia. Sin los paréntesis, pasas el objeto método — que sorted invocará con cada elemento.
Error: Crear closures en un loop capturando la variable del loop por referencia en lugar de por valor.
# ❌ Wrong funcs = [lambda: i for i in range(3)] print([f() for f in funcs]) # [2, 2, 2] — todas capturan la misma 'i' final # ✅ Right funcs = [lambda i=i: i for i in range(3)] print([f() for f in funcs]) # [0, 1, 2]
El argumento por defecto i=i evalúa i en el momento de creación de cada lambda, fijando ese valor en el objeto función en lugar de capturar la variable del scope externo.
Error: Mutar __defaults__ directamente cuando quieres cambiar valores por defecto — el tuple es inmutable pero si el default es un objeto mutable, lo compartirán todas las llamadas.
# ❌ Wrong
def append_to(value, lst=[]): # lst=[] se crea una sola vez, al definir la función
lst.append(value)
return lst
print(append_to(1)) # [1]
print(append_to(2)) # [1, 2] ← acumuló estado entre llamadas
# ✅ Right
def append_to(value, lst=None):
if lst is None:
lst = []
lst.append(value)
return lst
__defaults__ se evalúa una vez cuando Python ejecuta def. Si el valor por defecto es mutable, todas las invocaciones que no pasen ese argumento comparten el mismo objeto.
N° 61