inspect自省模块
Python运行时类型与对象自省
一、概述
inspect 是 Python 标准库中极为重要的一个模块,它提供了对活动对象进行自省(introspection)的能力。所谓"自省",就是让代码能够在运行时检查自身或其他对象的各种属性,包括类型、成员、源码、参数签名、调用栈帧等信息。该模块是构建调试器、测试框架、文档生成工具、依赖注入容器和元编程框架的基础设施。
inspect 模块主要提供以下四大类功能:
- 对象成员检查:获取对象的属性和方法列表,配合 predicate 过滤器按类型筛选
- 源码定位:获取对象的源代码、所属文件和行号
- 调用签名:解析函数的参数名称、类型、默认值、注解信息
- 堆栈自省:获取当前调用栈帧,追踪调用链,访问局部/全局变量
核心认知
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)
注意事项
getsource 和 getsourcelines 只能作用于 Python 模块中定义的对象,对 C 扩展实现的内置对象(如 len、str)会抛出 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 支持 bind 和 apply_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 配合 isfunction 或 ismethod 找到以 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
- 批量检查对象成员:
getmembers 比 dir() 更方便,且支持过滤
十、核心要点总结
- getmembers:遍历对象所有成员,支持 predicate 过滤,是 inspect 使用频率最高的函数
- getsource / getsourcelines:获取 Python 对象的源码,仅对 Python 实现的对象有效
- signature / Parameter:现代 API,完整解析函数参数信息,支持
bind 和 apply_defaults
- getfullargspec:旧版参数解析 API,理解其用法有助于维护遗留代码
- stack / currentframe:获取调用堆栈,是调试器和日志框架的核心依赖
- getclosurevars:探查闭包捕获的非局部变量和全局变量,理解 Python 作用域模型的关键工具
- is函数家族:超过 15 个类型判断函数,覆盖函数、类、生成器、协程、模块等所有内置类型
- 应用模式:装饰器参数转发、测试用例自动发现、依赖注入解析、调试器变量探查四大典型场景
- 最佳实践:优先使用
signature 而非 getfullargspec;注意 getmembers 可能触发 property 副作用;getsource 需要源码文件存在
- 性能注意:堆栈自省(stack/currentframe)相对开销较大,不适合在高频路径中调用
十一、进一步思考
进阶探索方向
- AST 层面的元编程:inspect 只能获取已存在的源码,结合
ast 模块可以在代码执行前分析和修改源码结构
- 与
sys.settrace 结合:trace 函数可以设置行级别的回调,配合 inspect 的帧自省可以构建完整的行级调试器
- ORM 中的字段映射:许多 ORM 框架利用 annotateions 自省配合 inspect 构建模型字段到数据库列的映射
- 序列化协议增强:使用 inspect 自动识别对象的可序列化字段,构建通用的序列化/反序列化框架
- 契约式设计:结合 signature 和类型注解,在运行时强制执行前置条件/后置条件检查
思考题
- 尝试用
inspect.stack 实现一个性能分析器,统计每个函数被调用的次数和总耗时
- 如何用
getmembers 配合 isfunction 实现一个插件系统,自动发现并注册插件中的处理函数?
- 在异步编程中,
iscoroutinefunction 和 iscoroutine 分别适用于什么判断场景?
- 使用
signature.bind 实现一个函数参数校验中间件,支持可选的类型校验规则