inspect自省模块

Python运行时类型与对象自省

学习主题: Python inspect 标准库模块

核心内容: 运行时类型检查、对象成员枚举、源码信息获取、函数签名解析、堆栈自省、闭包变量探查

应用场景: 装饰器构造、调试器开发、测试框架、序列化工具、文档生成、IDE智能提示

关键词: Python, inspect, 自省, 反射, getmembers, getsource, signature, 堆栈, 闭包, 函数签名, Parameter, 类型判断

一、概述

inspect 是 Python 标准库中极为重要的一个模块,它提供了对活动对象进行自省(introspection)的能力。所谓"自省",就是让代码能够在运行时检查自身或其他对象的各种属性,包括类型、成员、源码、参数签名、调用栈帧等信息。该模块是构建调试器、测试框架、文档生成工具、依赖注入容器和元编程框架的基础设施。

inspect 模块主要提供以下四大类功能:

核心认知

Python 中"一切皆对象"的理念是 inspect 发挥作用的基础。函数、类、模块甚至代码本身都是运行时对象,inspect 提供了统一且完备的 API 来探查这些对象的内部结构。

二、获取对象成员 —— getmembers

inspect.getmembers(object[, predicate]) 是最常用的自省函数之一。它返回对象所有可访问的成员(属性和方法),每个成员表示为 (name, value) 元组,按名称排序。第二个参数 predicate 是一个可选的过滤函数,只有满足条件的成员才会被返回。

基础用法

# 查看模块的所有成员 import inspect import json print(inspect.getmembers(json)) # 输出示例:[('JSONDecodeError', <class 'json.JSONDecodeError'>), ...] # 仅获取模块中的类 classes = inspect.getmembers(json, inspect.isclass) print(classes) # 输出:[('JSONDecoder', <class 'json.JSONDecoder'>), ('JSONEncoder', <class 'json.JSONEncoder'>)]

常用 predicate 过滤器

inspect 提供了丰富的判断函数,与 getmembers 配合使用可以精确筛选所需成员:

谓词函数 筛选目标 说明
inspect.isfunction 函数(不含内置函数) 严格意义上的 Python 函数对象
inspect.isclass 任何 class 定义的类型
inspect.ismethod 绑定方法 实例的绑定方法
inspect.ismodule 模块 import 导入的模块
inspect.isbuiltin 内置函数/方法 C 语言实现的内置函数
inspect.isroutine 可调用对象 函数、方法、内置函数等的并集
inspect.isgenerator 生成器对象 yield 表达式创建的对象
inspect.iscoroutinefunction async 函数 async def 定义的协程函数

实战:构建成员浏览器

def browse_object(obj, name="object"): """打印对象的详细成员信息,按类别分组""" print(f"=== 浏览对象: {name} ({type(obj).__name__}) ===") categories = { "方法": inspect.ismethod, "函数": inspect.isfunction, "类": inspect.isclass, "模块": inspect.ismodule, "内置": inspect.isbuiltin, "数据": None, # 不属于以上任何类别视为数据属性 } for cat_name, pred in categories.items(): if pred is not None: members = inspect.getmembers(obj, pred) else: all_members = inspect.getmembers(obj) classified = set() for p in categories.values(): if p is not None: classified.update(n for n, _ in inspect.getmembers(obj, p)) members = [(n, v) for n, v in all_members if n not in classified] if members: print(f"\n--- {cat_name} ({len(members)}个) ---") for name, val in members[:5]: # 最多显示5个 print(f" {name}: {type(val).__name__}") if len(members) > 5: print(f" ... 还有{len(members) - 5}个") # 使用示例 import datetime browse_object(datetime.datetime, "datetime.datetime")

注意事项

  • getmembers 会触发属性访问(property),可能产生副作用
  • 返回结果按成员名称字母序排列,而非定义顺序
  • 对于类对象,getmembers 也会包含继承的成员

三、获取源码信息

inspect 提供了三个核心函数来定位对象在源代码中的位置:getsource 返回完整源码,getfile 返回文件路径,getlineno 返回定义所在行号。

getsourcelines / getsource

import inspect import os def greet(name, greeting="Hello"): """向指定的名字打招呼""" return f"{greeting}, {name}!" # 获取源码行列表和起始行号 lines, start_line = inspect.getsourcelines(greet) print(f"起始行号: {start_line}") print(f"源码行数: {len(lines)}") print("源码内容:") print("".join(lines)) # 获取完整的源码字符串 source = inspect.getsource(greet) print("getsource 输出:") print(source)

getfile / getlineno

# 获取对象定义的文件路径 file_path = inspect.getfile(greet) print(f"定义文件: {file_path}") # 输出: 定义文件: /path/to/current/script.py # 获取对象定义的行号(仅返回第一行) line_no = inspect.getlineno(greet) print(f"定义行号: {line_no}") # 输出: 定义行号: 7 # 对内置对象无效,会抛出 TypeError try: inspect.getfile(len) except TypeError as e: print(f"内置对象无法获取: {e}")

实战:实现自省式文档生成器

def generate_docs(module): """扫描模块中的函数和类,生成 Markdown 文档""" lines = [f"# {module.__name__} 模块文档\n"] for name, obj in inspect.getmembers(module): if inspect.isfunction(obj): lines.append(f"## 函数: {name}\n") # 获取源码位置 try: file = inspect.getfile(obj) line_no = inspect.getlineno(obj) lines.append(f"- **源码位置**: {file}:{line_no}\n") except (TypeError, OSError): pass # 获取文档字符串 doc = inspect.getdoc(obj) if doc: lines.append(f"- **说明**: {doc}\n") # 获取签名 try: sig = inspect.signature(obj) lines.append(f"- **签名**: `{name}{sig}`\n") except (ValueError, TypeError): pass lines.append("\n---\n") return "\n".join(lines)

注意事项

  • getsourcegetsourcelines 只能作用于 Python 模块中定义的对象,对 C 扩展实现的内置对象(如 lenstr)会抛出 OSError
  • getfile 对内置对象和交互式 REPL 中定义的对象也会失败
  • getdoc 比直接访问 __doc__ 更智能,它会清理空白缩进并返回美化后的文档字符串

四、函数签名 —— signature 与 Parameter

inspect.signature(callable) 返回一个 Signature 对象,包含函数的全部参数信息。配合 Parameter 类,可以精确了解函数接受哪些参数、参数类型(位置/关键字)、默认值和注解。这是构建装饰器、参数校验器和 CLI 框架的核心工具。

Signature 和 Parameter 的核心属性

import inspect def complex_function( a, # 位置参数 b: int = 0, # 带默认值的关键字参数,有类型注解 *args, # 可变位置参数 c: str = "default", # 仅关键字参数 **kwargs # 可变关键字参数 ) -> bool: return True sig = inspect.signature(complex_function) print(f"完整签名: {sig}") print(f"返回类型: {sig.return_annotation}") print(f"参数数量: {len(sig.parameters)}") print()
# 遍历每个参数 for name, param in sig.parameters.items(): print(f"参数: {name}") print(f" 种类: {param.kind.name}") print(f" 默认值: {param.default}") print(f" 注解: {param.annotation}") print(f" 有默认值: {param.default is not param.empty}") print()

参数种类(Parameter.kind)

种类 含义 示例定义
POSITIONAL_ONLY 仅位置参数 def f(a, b, /):
POSITIONAL_OR_KEYWORD 位置或关键字 def f(a, b):(最常见)
VAR_POSITIONAL 可变位置参数 *args
KEYWORD_ONLY 仅关键字参数 *args, kw*, kw
VAR_KEYWORD 可变关键字参数 **kwargs

实战:参数校验装饰器

from functools import wraps def validate_types(func): """根据函数注解自动校验参数类型""" sig = inspect.signature(func) @wraps(func) def wrapper(*args, **kwargs): # 绑定参数到签名 bound = sig.bind(*args, **kwargs) bound.apply_defaults() # 校验每个参数的类型注解 for name, value in bound.arguments.items(): param = sig.parameters[name] if param.annotation is not param.empty: if not isinstance(value, param.annotation): raise TypeError( f"参数 '{name}' 需要 {param.annotation.__name__} 类型," f"但传入的是 {type(value).__name__}" ) return func(*args, **kwargs) return wrapper @validate_types def add(x: int, y: int) -> int: return x + y print(add(3, 5)) # OK: 8 print(add(3, "5")) # TypeError!

getfullargspec(旧版 API)

signature 引入之前,inspect.getfullargspec 是获取参数信息的传统方式。虽然现在推荐使用 signature,但大量存量代码仍在使用该 API,理解其返回结构对于维护老旧项目非常重要。

# getfullargspec 返回一个 FullArgSpec 具名元组 spec = inspect.getfullargspec(complex_function) print(f"参数名列表: {spec.args}") # ['a', 'b'] print(f"可变位置参数名: {spec.varargs}") # 'args' print(f"可变关键字参数名: {spec.varkw}") # 'kwargs' print(f"默认值: {spec.defaults}") # (0,) print(f"仅关键字参数: {spec.kwonlyargs}") # ['c'] print(f"仅关键字默认值: {spec.kwonlydefaults}") # {'c': 'default'} print(f"注解: {spec.annotations}") # {'b': int, 'c': str, 'return': bool}

signature vs getfullargspec

  • signature 是 Python 3.3+ 的现代 API,完整支持 PEP 362,推荐用于新项目
  • getfullargspec 是 Python 2 时代的遗留 API,不支持仅位置参数(PEP 570)
  • signature 对内置函数和 C 扩展函数的兼容性更好
  • signature 返回的 BoundArguments 支持 bindapply_defaults,方便实际调用

五、堆栈自省 —— 追踪调用链

堆栈自省是 inspect 模块中最"高级"也最强大的功能之一。它允许代码查看自己的调用上下文 —— 谁调用了它、调用者的局部变量是什么、调用发生在哪一行。这些信息对于调试器、日志框架和错误诊断工具至关重要。

核心函数对比

函数 返回结果 典型用途
inspect.stack() 完整的调用堆栈列表,每个元素是一个 FrameInfo 元组 调试、性能分析、完整调用链跟踪
inspect.trace() 当前异常发生时的堆栈帧列表 异常处理中获取异常发生的上下文
inspect.currentframe() 当前帧对象(最底层帧) 快速获取当前执行上下文

stack() 详解

import inspect def inner(): frame_info = inspect.stack() print("=== 完整调用堆栈 ===") for i, frame in enumerate(frame_info): print(f"#{i}: {frame.filename}:{frame.lineno} in {frame.function}") print(f" 代码: {frame.code_context[0].strip() if frame.code_context else ''}") def middle(): inner() def outer(): middle() outer() # 输出示例: # === 完整调用堆栈 === # #0: script.py:8 in inner ← 当前函数 # #1: script.py:18 in middle ← 上一层调用者 # #2: script.py:21 in outer ← 再上一层 # #3: script.py:23 in <module> ← 模块顶层

currentframe() 获取即时上下文

def whoami(): """返回当前函数的名称""" frame = inspect.currentframe() return frame.f_code.co_name def callers_name(): """返回调用当前函数的函数名称""" frame = inspect.currentframe() return frame.f_back.f_code.co_name def example(): print(f"当前函数: {whoami()}") print(f"调用者: {callers_name()}") example() # 输出: # 当前函数: example # 调用者: <module>

实战:智能日志装饰器

import inspect import logging from functools import wraps logger = logging.getLogger(__name__) def log_call(level=logging.INFO): """自动记录函数调用信息的装饰器,包含调用来源""" def decorator(func): sig = inspect.signature(func) @wraps(func) def wrapper(*args, **kwargs): # 获取调用者的信息 caller_frame = inspect.currentframe().f_back caller_info = inspect.getframeinfo(caller_frame) # 绑定参数 bound = sig.bind(*args, **kwargs) bound.apply_defaults() arg_str = ", ".join( f"{k}={v!r}" for k, v in bound.arguments.items() ) logger.log( level, "[来自 %s:%d (%s)] 调用 %s(%s)", caller_info.filename, caller_info.lineno, caller_info.function, func.__name__, arg_str, ) return func(*args, **kwargs) return wrapper return decorator @log_call() def process_order(order_id: int, amount: float): return {"status": "ok", "order": order_id} process_order(1001, 299.99)

帧对象(frame object)的核心属性

  • f_back:上一个帧(调用者),形成单向链
  • f_code:代码对象,包含 co_name(函数名)、co_filename(文件名)、co_varnames(局部变量名)
  • f_lineno:当前执行的行号
  • f_locals:当前帧的局部变量字典(可写!)
  • f_globals:当前帧的全局变量字典

六、闭包变量探查 —— getclosurevars

inspect.getclosurevars(func) 返回一个 ClosureVars 具名元组,其中包含函数闭包所引用的所有非局部变量和全局变量。这对于理解装饰器、闭包和偏函数的工作原理非常有帮助。

import inspect def make_counter(start=0): """创建一个计数器闭包""" count = start def counter(step=1): nonlocal count count += step return count return counter counter = make_counter(10) cv = inspect.getclosurevars(counter) print("非局部变量 (nonlocals):") for k, v in cv.nonlocals.items(): print(f" {k} = {v}") print("\n全局变量 (globals):") for k, v in cv.globals.items(): print(f" {k} = {v}") print("\n内置引用 (builtins):") print(f" {list(cv.builtins)}") # 输出: # 非局部变量 (nonlocals): # count = 10 # start = 10 # 全局变量 (globals): # inspect = <module 'inspect'> # 内置引用 (builtins): # ['print']

闭包变量的作用

getclosurevars 在以下场景中特别有用:

  • 调试装饰器链:确认装饰器是否正确捕获了外层变量
  • 序列化与反序列化:将闭包及其捕获的变量一起序列化
  • 安全审计:检查一个函数是否引用了意外的全局变量
  • 教学演示:直观展示 Python 闭包的内存模型

与 getsource 结合:完全还原函数

def inspect_function(func): """完整打印一个函数的所有自省信息""" print(f"====== 函数自省: {func.__name__} ======") # 1. 所在文件 try: print(f"所在文件: {inspect.getfile(func)}") except TypeError: print("所在文件: <built-in>") # 2. 签名 try: sig = inspect.signature(func) print(f"签名: {func.__name__}{sig}") except ValueError: print("签名: <无法获取>") # 3. 文档字符串 doc = inspect.getdoc(func) print(f"文档: {doc[:50] if doc else '无'}...") # 4. 闭包变量 if func.__closure__: cv = inspect.getclosurevars(func) print(f"闭包变量 (nonlocal): {cv.nonlocals}") print(f"闭包变量 (global): {cv.globals}") # 5. 注释 print(f"__annotations__: {getattr(func, '__annotations__', {})}") # 6. 源码(前5行) try: lines, start = inspect.getsourcelines(func) print(f"源码 (行 {start}-{start+len(lines)-1}):") print("".join(lines[:5])) if len(lines) > 5: print(f" ... (共{len(lines)}行)") except OSError: print("源码: <不可用>") print("=" * 50)

七、类型判断函数体系

inspect 提供了一套完备的类型判断函数,用于在运行时确定对象的种类。这些函数比 type()isinstance() 更精细,能够区分普通函数、生成器函数、协程函数以及绑定方法等细微的类型差异。

完整判断函数速查表

函数 判断目标 示例
isfunction Python 函数对象 def f(): pass
isgeneratorfunction 生成器函数 def g(): yield
iscoroutinefunction 协程函数 async def c(): pass
isasyncgenfunction 异步生成器 async def ag(): yield
ismethod 绑定方法 obj.method
ismethoddescriptor 方法描述符 str.split
isclass class A: pass
ismodule 模块 import os
iscode 代码对象 f.__code__
istraceback 回溯对象 sys.exc_info()[2]
isgenerator 生成器实例 g()
iscoroutine 协程实例 c()
isawaitable 可等待对象 await obj
isabstract 抽象基类 ABC 子类
isbuiltin 内置函数/方法 len, print

实战:通用序列化工具

import inspect import json class SmartSerializer: """利用 inspect 自省实现智能序列化""" @classmethod def can_serialize(cls, obj) -> bool: """判断对象是否可以被序列化""" if inspect.isroutine(obj): return False # 函数/方法不可直接 JSON 序列化 if inspect.isclass(obj): return False # 类不可序列化 if inspect.ismodule(obj): return False # 模块不可序列化 if inspect.isgenerator(obj): return False # 生成器不可序列化 return True @classmethod def to_dict(cls, obj) -> dict: """将任意对象转为可序列化的字典""" result = {} for name, value in inspect.getmembers(obj): # 跳过私有成员和特殊方法 if name.startswith('_'): continue # 跳过可调用对象 if inspect.isroutine(value) or inspect.isclass(value): continue try: json.dumps(value) result[name] = value except (TypeError, ValueError): pass return result

八、inspect 的实际应用场景

场景一:装饰器中的参数转发

使用 inspect.signature 可以让装饰器完全保留被装饰函数的签名信息,使得帮助文档(help())和 IDE 自动补全能够正确工作。

from functools import wraps def logger(func): @wraps(func) # 保留 __name__, __doc__ 等属性 def wrapper(*args, **kwargs): print(f"调用 {func.__name__}") return func(*args, **kwargs) # 手动复制签名(wraps 会自动处理,此处仅做示范) wrapper.__signature__ = inspect.signature(func) return wrapper

场景二:测试框架中的自动发现

Pytest、unittest 等测试框架使用 inspect 自动发现和收集测试用例。其核心逻辑是通过 getmembers 配合 isfunctionismethod 找到以 test_ 开头的函数。

import inspect def discover_tests(module): """扫描模块中所有测试函数""" tests = [] for name, obj in inspect.getmembers(module, inspect.isfunction): if name.startswith('test_'): # 获取测试函数的签名 sig = inspect.signature(obj) tests.append({ "name": name, "signature": str(sig), "doc": inspect.getdoc(obj), "file": inspect.getfile(obj), "line": inspect.getlineno(obj), }) return tests

场景三:依赖注入容器

import inspect class Container: """简单的依赖注入容器,基于 inspect 自动解析构造函数参数""" def __init__(self): self._providers = {} def register(self, cls): """注册一个类到容器""" self._providers[cls.__name__] = cls return cls def resolve(self, cls): """解析一个类的依赖并创建实例""" sig = inspect.signature(cls.__init__) kwargs = {} for name, param in sig.parameters.items(): if name == 'self': continue # 从注解类型推断依赖 if param.annotation is not param.empty: if param.annotation.__name__ in self._providers: kwargs[name] = self.resolve(param.annotation) return cls(**kwargs)

场景四:调试器中的变量检查

import inspect import sys def debug_vars(message=""): """打印当前作用域的所有局部变量(调试辅助函数)""" frame = inspect.currentframe().f_back locals_dict = frame.f_locals print(f"=== 调试: {message} ===") print(f"位置: {frame.f_code.co_filename}:{frame.f_lineno}") for k, v in locals_dict.items(): if not k.startswith('_'): print(f" {k} = {v!r} (type: {type(v).__name__})") print("=" * 30)

九、inspect 与其他自省机制的对比

Python 的自省能力并非只有 inspect 模块,但它是最全面、最标准化的选择。以下是几种自省方式的对比:

机制 能力范围 标准程度 适用场景
type() / isinstance() 基础类型判断 语言内置 大多数日常类型检查
dir() / vars() 属性名称列表 语言内置 简单自省、调试
__dict__ / __annotations__ 特定内部属性 语言内置 底层操作、序列化
inspect 模块 最全面 标准库 专业自省需求
typing 模块 泛型类型元数据 标准库 类型注解处理

选择建议

  • 简单类型检查:优先用 isinstance()issubclass()
  • 需要源码或签名:必须用 inspect
  • 需要堆栈信息:必须用 inspect
  • 批量检查对象成员getmembersdir() 更方便,且支持过滤

十、核心要点总结

十一、进一步思考

进阶探索方向

  • AST 层面的元编程:inspect 只能获取已存在的源码,结合 ast 模块可以在代码执行前分析和修改源码结构
  • sys.settrace 结合:trace 函数可以设置行级别的回调,配合 inspect 的帧自省可以构建完整的行级调试器
  • ORM 中的字段映射:许多 ORM 框架利用 annotateions 自省配合 inspect 构建模型字段到数据库列的映射
  • 序列化协议增强:使用 inspect 自动识别对象的可序列化字段,构建通用的序列化/反序列化框架
  • 契约式设计:结合 signature 和类型注解,在运行时强制执行前置条件/后置条件检查

思考题

  1. 尝试用 inspect.stack 实现一个性能分析器,统计每个函数被调用的次数和总耗时
  2. 如何用 getmembers 配合 isfunction 实现一个插件系统,自动发现并注册插件中的处理函数?
  3. 在异步编程中,iscoroutinefunctioniscoroutine 分别适用于什么判断场景?
  4. 使用 signature.bind 实现一个函数参数校验中间件,支持可选的类型校验规则