Frameworks, depuradores, sistemas de plugins, generadores de documentación y herramientas de prueba necesitan descubrir cómo fue definido un objeto Python. Comprueban si algo es función, clase, generator o coroutine, enumeran miembros, leen firmas, recuperan código fuente y examinan la pila de ejecución. El módulo inspect en Python reúne estas operaciones de introspección en la biblioteca estándar.
Esta guía explica getmembers(), los predicados is*, signature(), Signature.bind(), getsource(), unwrap(), getattr_static() y funciones de frames. Complementa nuestros artículos sobre descriptors en Python, singledispatch, copias de objetos, diagnóstico de rendimiento y contextvars.
Qué es la introspección
La introspección es la capacidad de un programa para examinar objetos durante la ejecución. Las funciones, clases, módulos y métodos de Python contienen metadatos como nombre, módulo, documentación, annotations y referencias al código compilado.
def calcular_total(valor: float, tasa: float = 0.1) -> float:
"""Calcula un total con una tasa."""
return valor * (1 + tasa)
print(calcular_total.__name__)
print(calcular_total.__doc__)
print(calcular_total.__annotations__)inspect ofrece una interfaz uniforme y herramientas aplicables a varias categorías.
Identificar categorías de objetos
Predicados como isfunction(), ismethod(), isclass() e ismodule() expresan claramente la intención.
import inspect
class Servicio:
def ejecutar(self):
return "ok"
servicio = Servicio()
print(inspect.isclass(Servicio))
print(inspect.isfunction(Servicio.ejecutar))
print(inspect.ismethod(servicio.ejecutar))
print(inspect.isroutine(servicio.ejecutar))El acceso mediante la clase devuelve la función definida; mediante una instancia produce un método vinculado. Esta diferencia importa en registradores e inyección de dependencias.
Generators, coroutines y async generators
El módulo distingue las funciones que crean objetos asíncronos de los objetos ya creados.
import inspect
async def buscar():
return 42
async def eventos():
yield "inicio"
def numeros():
yield 1
print(inspect.iscoroutinefunction(buscar))
print(inspect.isasyncgenfunction(eventos))
print(inspect.isgeneratorfunction(numeros))Después de llamar, usa iscoroutine(), isasyncgen(), isgenerator() o isawaitable(). Espera o cierra los objetos creados en pruebas.
Enumerar miembros con getmembers()
getmembers() devuelve pares (nombre, valor) ordenados.
class Producto:
categoria = "general"
def __init__(self, nombre):
self.nombre = nombre
def resumen(self):
return self.nombre
for nombre, valor in inspect.getmembers(Producto):
if not nombre.startswith("__"):
print(nombre, valor)Un predicado opcional filtra la salida:
metodos = inspect.getmembers(Producto, inspect.isfunction)
print([nombre for nombre, _ in metodos])Esta combinación resulta útil para descubrir handlers, comandos y pruebas.
getmembers() puede ejecutar código
La búsqueda normal activa descriptors, properties, __getattr__() y __getattribute__(). Inspeccionar un objeto puede ejecutar lógica.
class Ejemplo:
@property
def peligroso(self):
print("property ejecutada")
return 10
inspect.getmembers(Ejemplo())Esto puede provocar efectos secundarios en herramientas de documentación y objetos no confiables.
Inspección pasiva con getmembers_static()
Desde Python 3.11, getmembers_static() evita la resolución dinámica.
miembros = inspect.getmembers_static(Ejemplo())
for nombre, valor in miembros:
if nombre == "peligroso":
print(valor) # descriptor property, sin ejecutar getterPuede devolver el descriptor en vez del valor y omitir atributos dinámicos. Elige según necesites comportamiento real o estructura estática.
getattr_static()
getattr_static() recupera un atributo sin activar descriptors, __getattr__ o __getattribute__.
descriptor = inspect.getattr_static(Ejemplo, "peligroso")
print(type(descriptor))Es adecuado para analizadores y depuradores. Resolver manualmente un descriptor aún puede ejecutar código.
Documentación con getdoc()
getdoc() limpia indentación y puede heredar documentación de clases, métodos, propiedades y descriptors.
print(inspect.getdoc(calcular_total))cleandoc() normaliza una cadena de documentación independiente.
Localizar archivo y módulo
getfile(), getsourcefile() y getmodule() ayudan a herramientas de desarrollo.
print(inspect.getfile(calcular_total))
print(inspect.getsourcefile(calcular_total))
print(inspect.getmodule(calcular_total))Built-ins y extensiones en C pueden no tener archivo fuente Python. Captura TypeError y admite None.
Recuperar código fuente
getsource() devuelve texto fuente para módulos, clases, funciones, métodos, frames, tracebacks y objetos de código compatibles.
try:
fuente = inspect.getsource(calcular_total)
print(fuente)
except (OSError, TypeError):
print("Código fuente no disponible")La documentación oficial de inspect indica que OSError aparece cuando la fuente no puede recuperarse y TypeError en muchos built-ins. El código interactivo, dinámico o empaquetado también puede no tener fuente accesible.
getsourcelines() y comentarios
getsourcelines() devuelve líneas y número inicial.
lineas, inicio = inspect.getsourcelines(calcular_total)
print(inicio)
print("".join(lineas))getcomments() busca comentarios anteriores a la definición. Prefiere docstrings y metadatos explícitos para contratos estables.
Firmas con signature()
signature() es la API recomendada para parámetros de callables.
from inspect import signature
sig = signature(calcular_total)
print(sig)
print(sig.return_annotation)
for nombre, parametro in sig.parameters.items():
print(nombre, parametro.kind, parametro.default, parametro.annotation)Entiende funciones, clases, métodos, functools.partial y muchos objetos callable.
Tipos de Parameter
Cada parámetro tiene un kind:
POSITIONAL_ONLYantes de/;POSITIONAL_OR_KEYWORDpara el caso común;VAR_POSITIONALpara*args;KEYWORD_ONLYdespués de*;VAR_KEYWORDpara**kwargs.
def ejemplo(a, /, b=2, *args, c, **kwargs):
pass
for param in inspect.signature(ejemplo).parameters.values():
print(param.name, param.kind.description)Los frameworks pueden generar formularios, validadores y llamadas adaptadas.
Ausencia frente a None
Parameter.empty significa que no existe default o annotation. Es diferente de un valor explícito None.
param = inspect.signature(calcular_total).parameters["tasa"]
if param.default is inspect.Parameter.empty:
print("Obligatorio")Compara el marcador por identidad.
Validar llamadas con Signature.bind()
bind() asocia argumentos como una llamada real y lanza TypeError ante una combinación inválida.
sig = inspect.signature(calcular_total)
try:
ligados = sig.bind(100, tasa=0.2)
print(ligados.arguments)
except TypeError as error:
print("Argumentos inválidos:", error)Permite validar plugins y rutas antes de ejecutar.
bind_partial() y defaults
bind_partial() permite omitir argumentos obligatorios, como functools.partial().
parcial = sig.bind_partial(tasa=0.15)
print(parcial.arguments)BoundArguments.apply_defaults() añade defaults, una tupla vacía para *args y un diccionario vacío para **kwargs.
ligados = sig.bind(100)
ligados.apply_defaults()
print(ligados.arguments)Llamar con BoundArguments
Las propiedades args y kwargs permiten ejecutar después de validar o transformar.
ligados = sig.bind("100", tasa="0.2")
ligados.arguments["valor"] = float(ligados.arguments["valor"])
ligados.arguments["tasa"] = float(ligados.arguments["tasa"])
resultado = calcular_total(*ligados.args, **ligados.kwargs)No uses annotations automáticamente como conversores; pueden ser objetos arbitrarios.
Annotations y riesgo de evaluación
signature() puede conservar annotations como cadenas o resolverlas. eval_str y el formato controlan el comportamiento. Evaluar cadenas puede ejecutar código arbitrario.
Para contenido no confiable, usa eval_str=False y formato textual. Python 3.14 integra annotation_format con annotationlib.Format.
Decorators y __wrapped__
Los decorators pueden ocultar nombre, docs y firma. functools.wraps() copia metadatos y crea __wrapped__.
from functools import wraps
def registrar(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("Llamando", func.__name__)
return func(*args, **kwargs)
return wrapper
@registrar
def sumar(a: int, b: int = 0) -> int:
return a + b
print(inspect.signature(sumar))La documentación oficial de functools explica cómo wraps conserva metadatos y acceso a la función original.
unwrap()
inspect.unwrap() sigue la cadena __wrapped__.
original = inspect.unwrap(sumar)
print(original.__name__)Detecta ciclos y lanza ValueError. El callback stop permite detenerse en un wrapper.
follow_wrapped
signature() sigue wrappers por defecto. Usa follow_wrapped=False para analizar el wrapper.
print(inspect.signature(sumar, follow_wrapped=True))
print(inspect.signature(sumar, follow_wrapped=False))Ayuda a depurar decorators que añaden comportamiento.
Modificar Signature y Parameter
Estos objetos son inmutables. Usa replace() o copy.replace().
sig = inspect.signature(sumar)
nueva = sig.replace(return_annotation="numero")
print(nueva)Cambiar metadatos no cambia el comportamiento. Contrato e implementación deben mantenerse sincronizados.
Herencia y resolución de métodos
getmro() devuelve el orden de resolución.
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass
print(inspect.getmro(D))Explica qué implementación selecciona la herencia múltiple. getclasstree() organiza clases jerárquicamente.
Closures y variables externas
getclosurevars() informa nombres no locales, globales, built-ins y no resueltos.
factor = 10
def crear():
adicional = 2
def calcular(valor):
return valor * factor + adicional
return calcular
print(inspect.getclosurevars(crear()))Es útil para depuración, pero expone referencias vivas. No registres secretos.
Estado de generators y coroutines
getgeneratorstate() informa si un generator está creado, ejecutándose, suspendido o cerrado.
def contador():
yield 1
yield 2
gen = contador()
print(inspect.getgeneratorstate(gen))
next(gen)
print(inspect.getgeneratorstate(gen))Hay funciones equivalentes para coroutines y async generators.
Variables locales vivas
getgeneratorlocals(), getcoroutinelocals() y getasyncgenlocals() recuperan estado local mientras existe un frame.
Dependen de detalles del intérprete y pueden devolver mappings vacíos. No bases reglas de negocio en ellos.
Frames y pila de ejecución
currentframe(), stack(), trace(), getouterframes() y getinnerframes() ayudan a depuradores e informes.
frame = inspect.currentframe()
try:
if frame is not None:
info = inspect.getframeinfo(frame)
print(info.filename, info.lineno, info.function)
finally:
del framecurrentframe() puede devolver None en implementaciones sin soporte de frames Python.
Los frames pueden retener memoria
Los frames referencian variables locales y frames externos. Conservar uno puede crear ciclos y prolongar la vida de grafos grandes.
Elimina referencias en finally. Si guardas un frame temporalmente, llama a frame.clear() al terminar. Es esencial en servidores persistentes.
Introspección no equivale a API pública
Encontrar un atributo no significa que sea estable o soportado. Nombres con underscore, campos de code objects y detalles de frames pueden ser internos.
Los plugins deben declarar interfaces y versiones. Usa introspección para adaptación y diagnóstico, no para sustituir especificaciones.
Compatibilidad entre implementaciones
Algunos built-ins de CPython no proporcionan firma completa. Descriptors y code objects pueden variar entre intérpretes.
Captura TypeError, ValueError y OSError, ofrece alternativas y prueba todos los intérpretes soportados.
Ejemplo: registro de handlers
def registrar_handler(func):
if not inspect.isfunction(func) and not inspect.ismethod(func):
raise TypeError("El handler debe ser función o método")
sig = inspect.signature(func)
parametros = list(sig.parameters.values())
if not parametros:
raise TypeError("El handler debe recibir un evento")
return {
"callable": func,
"firma": sig,
"documentacion": inspect.getdoc(func) or "",
}Antes de ejecutar, usa bind() con el evento y dependencias disponibles.
Errores frecuentes
- Usar
getmembers()en objetos no confiables sin considerar properties. - Suponer que todo callable tiene fuente y firma.
- Evaluar annotations de cadena sin revisar seguridad.
- Crear decorators sin
functools.wraps(). - Confundir función de clase con método vinculado.
- Conservar frames y crear ciclos.
- Depender de detalles exclusivos de CPython.
- Usar introspección en lugar de un contrato de plugins.
Buenas prácticas
- Usa predicados
is*para expresar intención. - Prefiere
signature()a APIs antiguas. - Usa
getmembers_static()para inspección pasiva. - Trata fuente y firma ausentes como casos normales.
- Conserva
__wrapped__conwraps(). - Evita evaluar annotations no confiables.
- Libera frames en
finally. - Prueba todas las implementaciones soportadas.
Conclusión
El módulo inspect en Python examina objetos vivos, clases, funciones, firmas, código fuente, closures, generators y pilas del intérprete. Permite construir documentación, registradores, depuradores y validadores adaptativos.
La introspección exige cuidado porque el acceso a atributos puede ejecutar descriptors, las annotations pueden implicar evaluación y los frames pueden retener grafos grandes. Al elegir APIs estáticas cuando corresponde, tolerar metadatos ausentes, preservar wrappers y liberar frames, aprovechas la flexibilidad dinámica de Python sin convertir el diagnóstico en efectos secundarios o fugas de memoria.







