inspect模块 — 检查对象信息

Python标准库精讲专题 · 类型与元编程篇 · 掌握对象内省工具

专题:Python标准库精讲系统学习

关键词:Python, 标准库, inspect, 内省, 对象检查, getsource, signature, getmembers, stack, 反射

一、inspect模块概述

inspect 是 Python 标准库中用于对象内省(introspection)的核心模块。它提供了丰富的工具函数,用于检查运行时的 Python 对象——包括模块、类、函数、方法、回溯对象、帧对象以及代码对象——并从中提取出源代码、参数签名、调用栈等元信息。内省能力是 Python 动态特性的重要体现,也是元编程、调试工具、测试框架和文档生成系统的基石。

要理解 inspect 的价值,需要先厘清几个相关概念:自省(introspection)指代码在运行时观察自身类型和属性的能力,例如执行 type(obj) 或 obj.__class__ 来获取对象的类型;反射(reflection)则是比自省更进一步的能力,不仅能够观察对象,还能在运行时动态修改对象的行为,例如通过 setattr 动态设置属性、通过 __getattr__ 实现动态方法分发。inspect 模块主要服务于"自省"层面,但它是实现反射和元编程的基础工具。

inspect 模块设计为"一站式"自省工具库,主要解决了以下几个常见需求:第一,类型判断——判断一个对象是模块、函数、类、生成器还是协程等;第二,成员枚举——获取对象的所有属性和方法;第三,源码定位——找出对象的定义文件和行号,提取源代码文本;第四,签名解析——解析函数的参数名称、类型标注、默认值和种类(位置参数、关键字参数、可变参数等);第五,调用栈分析——获取当前程序的调用堆栈,查看每一层的局部变量和上下文。

与其他语言的同类工具相比,Python 的 inspect 模块功能更加统一和全面。Java 的反射 API 分散在 java.lang.reflect 包中,需要理解 Class、Method、Field 等多个类体系;而 Python 的 inspect 将所有自省功能集中在单一模块中,函数命名直观(ismodule、getsource、signature),使用门槛较低。但 inspect 并非万能的——对于 C 扩展模块中定义的函数(例如大多数标准库的内置函数),getsource 方法将无法获取源代码,只能返回空值或抛出 OSError。

核心定位:inspect 是 Python 自省生态的"瑞士军刀",它不引入新的类或对象模型,而是提供一组纯函数来检视 Python 现有的对象体系。熟练掌握 inspect 是深入 Python 元编程、构建框架和工具链的必备技能。

二、类型检查

inspect 模块提供了十多组"is*"类型的谓词函数,用于判断对象是否为特定类型。这些函数比内置的 isinstance() 检查更精确,因为它们不仅检查对象的类型,还会考虑对象的内部结构和行为特征。例如,inspect.isfunction() 和 inspect.ismethod() 可以区分普通函数和绑定方法,而单纯的 type() 结果可能难以区分这些边界情况。

2.1 模块与包

inspect.ismodule(object) 判断对象是否为模块对象(module)。模块是 Python 组织代码的基本单元,检查结果与 isinstance(object, types.ModuleType) 等价。此函数在框架加载插件时尤其有用——当需要遍历所有已导入模块或动态加载模块时,可以用它来验证加载结果是否为合法的模块对象。

2.2 函数与类

inspect.isfunction(object) 判断对象是否为 Python 函数,这里指的是使用 def 语句定义的普通函数,包括 lambda 表达式。需要注意的是,内置函数(如 len、print)不属于此范畴——它们属于 builtin_function_or_method 类型,需要使用 inspect.isbuiltin() 来检查。inspect.isgeneratorfunction() 则可进一步判断一个函数是否是生成器函数(函数体内包含 yield 语句)。

inspect.isclass(object) 判断对象是否为类(class)。在元编程场景中,你可能需要区分一个对象是类还是该类的实例——例如在类装饰器中,装饰器接收的参数可能是一个类对象,使用 isclass 可以做出明确判断。

2.3 方法与绑定

inspect.ismethod(object) 判断对象是否为绑定方法(bound method),即通过实例访问的函数。例如 obj.method 返回的就是一个绑定方法对象,它与类上的原始函数不同——绑定方法已经将 self 参数固定为 obj。与之相对,inspect.isfunction() 判断的是未经绑定的原始函数。理解这一区别对于调试和框架开发非常重要:绑定方法在调用时会自动传入 self,而原始函数则需要手动传入实例。

2.4 生成器与协程

Python 3 引入了丰富的异步编程模型,inspect 模块也提供了相应的检查函数:inspect.isgenerator(object) 检查对象是否为生成器迭代器(generator iterator),即调用生成器函数后返回的对象;inspect.iscoroutine(object) 判断是否为协程对象(coroutine),即 async def 函数调用后返回的对象;inspect.iscoroutinefunction(object) 判断一个函数是否为协程函数(async def 定义的函数);inspect.isasyncgen(object) 和 inspect.isasyncgenfunction(object) 分别用于判断异步生成器和异步生成器函数。这些函数在后端 Web 框架(如 FastAPI、Sanic)的底层实现中被广泛用于判断路由处理函数的类型。

谓词函数检查目标典型用途
ismodule(obj)模块对象插件加载验证
isfunction(obj)Python 函数装饰器内部检查
ismethod(obj)绑定方法事件分发系统
isclass(obj)类对象元编程、ORM 映射
isgenerator(obj)生成器迭代器流式处理框架
iscoroutine(obj)协程对象异步框架底层
isbuiltin(obj)内置函数/方法标准库扩展判断
isabstract(obj)抽象基类接口验证
import inspect def sample(x): return x * 2 class MyClass: def method(self): pass print(inspect.isfunction(sample)) # True print(inspect.ismethod(MyClass().method)) # True print(inspect.isclass(MyClass)) # True print(inspect.isgenerator((x for x in []))) # True # 注意:内置函数是 builtin,不是 function print(inspect.isfunction(len)) # False print(inspect.isbuiltin(len)) # True

三、成员与属性

获取对象的所有成员是自省最基础也是最常见的需求。inspect 模块的 getmembers() 函数可以返回对象的所有成员(属性和方法)列表,每个成员以 (name, value) 二元组的形式呈现。相比内置的 dir() 函数只返回名称列表,getmembers 同时返回名称和值,使用起来更加便捷。更重要的是,getmembers 还接受一个可选的 predicate 参数用于过滤——例如 getmembers(obj, inspect.ismethod) 将只返回对象中的方法成员。

3.1 getmembers 的高级过滤

getmembers 的 predicate 参数极大简化了成员筛选工作。常见的过滤模式包括:getmembers(module, inspect.isfunction) 获取模块中定义的所有函数;getmembers(obj, inspect.isclass) 获取对象的所有嵌套类;getmembers(module, lambda m: inspect.isfunction(m) and m.__name__.startswith('_')) 获取模块中的私有函数。这种函数式过滤方式比手动遍历 dir() 结果并逐一检查更加简洁高效。

3.2 classify_class_attrs 类属性分类

inspect.classify_class_attrs(cls) 是一个强大的类属性分析工具。它返回一个命名元组列表,每个元素包含四个字段:name(属性名)、kind(属性类别,为 'static method' / 'class method' / 'method' / 'property' / 'data' 之一)、defining_class(定义该属性的类,考虑到继承)、object(属性值本身)。这个函数对于理解类继承体系中的属性来源非常有帮助——它能够告诉你一个方法是定义在当前类还是从父类继承的,以及它是普通方法还是静态方法。

3.3 其他成员查询

getclosurevars(func) 返回函数闭包中引用的自由变量(free variables),以 namedtuple 形式返回,包含 nonlocal 变量和全局变量。getannotations(obj) 获取对象的类型注解(type annotations)字典,适用于函数参数注解和变量注解,是 Python 3.10+ 中 typing.get_type_hints() 的底层支持。理解这些底层工具对于构建类型检查器、序列化框架和依赖注入系统至关重要。

import inspect import math # 获取模块中所有函数 funcs = inspect.getmembers(math, inspect.isfunction) print([name for name, _ in funcs[:5]]) # ['acos', 'acosh', 'asin', 'asinh', 'atan'] # 获取模块中所有公共名称(不以下划线开头) pub = [(n, v) for n, v in inspect.getmembers(math) if not n.startswith('_')] # 分析类的属性分类 class Base: x = 10 def foo(self): pass @staticmethod def bar(): pass @classmethod def baz(cls): pass class Derived(Base): y = 20 def qux(self): pass for attr in inspect.classify_class_attrs(Derived): print(f"{attr.name:8} | {attr.kind:15} | {attr.defining_class.__name__}") # x | data | Base # y | data | Derived # foo | method | Base # bar | static method | Base # baz | class method | Base # qux | method | Derived

四、源代码与文件

inspect 模块提供了从运行时代码对象反向定位源文件的完整工具链。这些函数在调试器、分析器、文档生成器和测试覆盖率工具中发挥着核心作用。Python 的函数、类和模块对象都带有 __code__ 属性,其中包含了指向源文件的文件名和行号信息,inspect 的这些函数正是基于这些底层属性构建的便捷接口。

4.1 getsource 系列——获取源代码

inspect.getsource(object) 是最常用的源码获取函数,它能返回对象的完整源代码文本。对于函数、类、方法、生成器、协程以及部分模块,只要其定义在 .py 文件中且文件未被删除,getsource 就能提取出对应的源码。其底层实现逻辑是:首先通过 getsourcefile 找到源文件路径,再通过 getsourcelines 获取具体的行号范围,最后读取文件中的相应行。如果对象定义在交互式环境(REPL)或 C 扩展中,getsource 将抛出 OSError。

inspect.getsourcelines(object) 返回一个 (lines, lineno) 的二元组,其中 lines 是源代码行的列表,lineno 是起始行号。这种方式适合需要对源码进行逐行处理或语法分析的场景。inspect.getsource(object) 实际上就是调用 getsourcelines 后将行列表拼接为单一字符串。

4.2 getsourcefile 与 getfile——定位源文件

inspect.getsourcefile(object) 返回定义该对象的源文件路径(字符串)。它会尝试解析对象的 __module__ 和 __code__.co_filename 属性,并检查文件是否实际存在于磁盘上。与直接读取 __code__.co_filename 相比,getsourcefile 会进行额外的规范化处理(将相对路径转换为绝对路径)。inspect.getfile(object) 的功能与 getsourcefile 类似,但限制更少——它还会返回 .pyc 字节码文件或 C 扩展的 .pyd/.so 文件路径,而不仅仅返回 .py 源文件。当只需要知道对象定义在哪个文件而不关心是否能获取源码时,getfile 更加适用。

4.3 getlineno——获取行号

inspect.getlineno(object) 返回对象在其源文件中定义的行号。对于某些无法获取行号的对象(如 C 扩展函数),会返回 None。行号信息在错误报告和日志系统中非常有用——当需要精确记录某个函数或类在源码中的位置时,getlineno 可以提供比 __code__.co_firstlineno 更准确的数值,因为它会考虑装饰器对行号的影响。

import inspect import os # 获取模块的源文件路径 print(inspect.getfile(os)) # 例如: /usr/lib/python3.12/os.py print(inspect.getsourcefile(os)) # 同上(如果 .py 文件存在) # 定义一个小函数用于演示 def greet(name): """向某人打招呼""" return f"Hello, {name}!" # 获取源代码 print(inspect.getsource(greet)) # def greet(name): # """向某人打招呼""" # return f"Hello, {name}!" # 获取源代码行和起始行号 lines, start = inspect.getsourcelines(greet) print(f"起始行: {start}") print(f"行数: {len(lines)}") # 获取行号 print(inspect.getlineno(greet)) # 输出 greet 的定义行号

五、函数签名

函数签名(function signature)是函数接口的形式化描述,包括参数名称、参数种类(positional-only、positional-or-keyword、var-positional、keyword-only、var-keyword)、默认值和类型注解。inspect.signature() 函数返回一个 Signature 对象,它完整地描述了一个可调用对象的参数结构,是构建类型检查器、参数校验框架、自动生成 API 文档和依赖注入系统的核心工具。

5.1 Signature 与 Parameter 对象

inspect.signature(callable) 返回一个 inspect.Signature 实例。Signature 对象包含一个 parameters 属性,它是一个从参数名到 Parameter 对象的有序映射(OrderedDict)。每个 Parameter 对象包含以下关键属性:name 是参数名称;default 是参数的默认值(如果没有默认值则为 Parameter.empty);annotation 是类型注解(如果没有注解则为 Parameter.empty);kind 是参数种类,由 Parameter 类的枚举常量表示。

Parameter.kind 取值包括五种:POSITIONAL_ONLY(仅限位置参数,如 def f(a, /) 中的 a);POSITIONAL_OR_KEYWORD(位置或关键字参数,即普通参数);VAR_POSITIONAL(可变位置参数,即 *args);KEYWORD_ONLY(仅限关键字参数,即 * 或 *args 之后的参数);VAR_KEYWORD(可变关键字参数,即 **kwargs)。理解这五种参数种类对于设计灵活且健壮的 API 至关重要。

5.2 Signature 对象的操作方法

Signature 对象提供了 bind(*args, **kwargs) 和 bind_partial(*args, **kwargs) 方法。bind 方法将传入的实际参数绑定到签名的参数上,返回一个 BoundArguments 实例。如果参数不匹配(如缺少必需参数、传入了未定义的参数),bind 会抛出 TypeError。这为参数校验提供了便捷的方式——你不需要手动编写参数检查逻辑,只需调用 signature(func).bind(*args, **kwargs) 即可验证参数的有效性。bind_partial 则允许部分绑定,不要求所有参数都提供,适合需要逐步构建参数的应用场景。

BoundArguments 对象包含 args 和 kwargs 属性,分别对应位置参数和关键字参数的最终绑定结果。它还支持 iterate 方法,可以按参数定义的顺序遍历所有参数及其对应的值。

5.3 从函数到签名——完整流程

以下示例展示了如何使用 signature() 解析不同类型的函数签名,以及如何利用 bind 进行参数校验。这在实际框架开发中非常实用——例如 Web 框架的路由处理器参数绑定、依赖注入容器的参数解析等场景。

import inspect from inspect import Parameter # 定义一个包含各种参数类型的函数 def example(a, b=10, /, c, d="hello", *args, e, **kwargs): pass # 获取签名 sig = inspect.signature(example) print(sig) # (a, b=10, /, c, d='hello', *args, e, **kwargs) # 遍历参数 for name, param in sig.parameters.items(): print(f"{name:6} kind={param.kind.name:25} default={param.default}") # a kind=POSITIONAL_ONLY default= # b kind=POSITIONAL_ONLY default=10 # c kind=POSITIONAL_OR_KEYWORD default= # d kind=POSITIONAL_OR_KEYWORD default=hello # args kind=VAR_POSITIONAL default= # e kind=KEYWORD_ONLY default= # kwargs kind=VAR_KEYWORD default= # 使用 bind 进行参数校验 try: sig.bind(1, 2, 3, e=4) print("绑定成功") except TypeError as err: print(f"绑定失败: {err}") # 使用 annotation 信息 def typed_func(x: int, y: str = "") -> bool: return isinstance(x, int) and isinstance(y, str) sig2 = inspect.signature(typed_func) for name, param in sig2.parameters.items(): print(f"{name}: {param.annotation}") # x: # y: print(f"返回值: {sig2.return_annotation}") # 返回值:

六、调用栈

调用栈(call stack)是程序运行时函数调用的轨迹记录,反映了当前执行点是如何通过一系列函数调用到达的。inspect 模块提供了 stack()、trace() 和 currentframe() 等函数,使 Python 程序能够检查和遍历自身的调用栈。这在调试器、日志系统和性能分析器中有着广泛的应用。

6.1 stack() 与 trace()

inspect.stack(context=1) 返回当前调用栈的帧记录列表(从调用者开始)。每个帧记录是一个 namedtuple,包含以下字段:frame(帧对象)、filename(源文件名)、lineno(当前行号)、function(函数名)、code_context(上下文代码行列表,行数为 context 参数指定)、index(当前行在 code_context 中的索引)。stack 函数的 context 参数控制返回的上下文行数——设为 0 则不返回代码行,设为 n 则返回当前行上下各 n 行(共 2n+1 行)。增加 context 值可以提供更多上下文信息,但会增加开销。需要注意的是,inspect.stack() 是一个开销较大的操作——它会遍历整个帧栈并读取源文件——因此在生产环境的性能关键路径上应谨慎使用。

inspect.trace(context=1) 返回当前线程的跟踪记录列表,与 sys.settrace 配合使用,用于实现调试器和性能分析器。普通应用开发中更常用的是 stack()。

6.2 currentframe() 与 getframeinfo

inspect.currentframe() 返回当前线程的栈顶帧对象(frame object),底层是通过 sys._getframe(1) 实现的。帧对象包含了执行上下文的所有信息:f_locals(局部变量字典)、f_globals(全局变量字典)、f_code(代码对象)、f_lineno(当前行号)、f_back(上一帧)等。inspect.getframeinfo(frame, context=1) 将帧对象转换为更易用的 FrameInfo 对象,返回的是与 stack() 记录相同的 namedtuple 格式,包含了文件名、行号、函数名和上下文代码行。

6.3 getargvalues——获取帧参数

inspect.getargvalues(frame) 返回帧对象的参数信息,以 ArgInfo namedtuple 的形式呈现,包含四个字段:args(参数名列表)、varargs(*args 参数名,如有)、keywords(**kwargs 参数名,如有)、locals(帧的局部变量字典)。这个函数在调试器中非常实用——当调试器暂停在某个断点时,可以通过 getargvalues 快速查看当前函数的参数及其值。在 Python 3.5+ 中,官方更推荐使用 signature 对象配合 bind 来获取更精确的参数信息,但 getargvalues 在简单调试场景中仍然很方便。

import inspect def inner(x, y): return middle(x + y) def middle(z): return outer(z * 2) def outer(w): # 获取当前调用栈 stack = inspect.stack() for i, frame in enumerate(stack): print(f"帧 {i}: {frame.filename}:{frame.lineno} in {frame.function}") return w + 1 inner(10, 20) # 输出示例(行号视实际定义位置而定): # 帧 0: demo.py:12 in outer ← 当前帧 # 帧 1: demo.py:8 in middle ← 调用者 # 帧 2: demo.py:5 in inner ← 更上层 # 帧 3: demo.py:23 in <module> ← 顶层 # 使用 currentframe 获取当前帧信息 frame = inspect.currentframe() info = inspect.getframeinfo(frame) print(f"当前文件: {info.filename}, 行号: {info.lineno}") # 获取帧的局部变量 def show_locals(a, b=5): c = a + b frame = inspect.currentframe() args_info = inspect.getargvalues(frame) print(f"参数: {args_info.args}") print(f"局部变量: {args_info.locals}") show_locals(3) # 参数: ['a', 'b'], 局部变量: {'a': 3, 'b': 5, 'c': 8}

七、实战案例与总结

理论知识只有应用到实际场景中才能真正掌握。下面通过三个典型的生产级案例,展示 inspect 模块在日常开发中的强大威力。这些案例涵盖了装饰器、参数校验和文档生成三个最常见的使用方向。

7.1 案例一:智能日志装饰器

利用 signature 和 stack 构建一个能够自动记录函数调用信息的装饰器。这个装饰器不需要手动拼接日志消息,而是通过 inspect 自动获取函数名、参数名和参数值,并附上调用者的位置信息。这种方式比手动写 print 或 logger.info 更加规范和便捷。

import inspect import functools def log_call(func): @functools.wraps(func) def wrapper(*args, **kwargs): sig = inspect.signature(func) bound = sig.bind(*args, **kwargs) bound.apply_defaults() # 获取调用者信息 caller = inspect.stack()[1] params = ", ".join( f"{k}={v!r}" for k, v in bound.arguments.items() ) print(f"[LOG] {func.__name__}({params}) " f"called from {caller.function} at {caller.filename}:{caller.lineno}") return func(*args, **kwargs) return wrapper @log_call def add(x, y=0): return x + y add(3, y=4) # [LOG] add(x=3, y=4) called from <module> at demo.py:25

7.2 案例二:通用参数校验框架

结合 signature 的 bind 方法和类型注解,构建一个轻量级的参数校验器。它可以自动检查传入参数的类型是否符合类型注解的约定,在函数执行前就抛出清晰的错误信息。这种方式比在函数体内手动编写 isinstance 检查更加优雅,且校验逻辑可以复用到多个函数上。

import inspect import functools def validate(func): sig = inspect.signature(func) @functools.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] hint = param.annotation if hint is not param.empty and hint is not inspect.Parameter.empty: if not isinstance(value, hint): raise TypeError( f"参数 '{name}' 期望类型 {hint.__name__}," f"实际得到 {type(value).__name__} = {value!r}" ) return func(*args, **kwargs) return wrapper @validate def divide(a: int, b: int) -> float: return a / b divide(10, 2) # 5.0 # divide(10, "2") # TypeError: 参数 'b' 期望类型 int,实际得到 str = '2'

7.3 案例三:自动 API 文档生成

利用 getsource、signature 和 getdoc 将任意模块中的函数自动转换为 Markdown 格式的 API 文档。这是很多自动化文档工具(如 Sphinx 的 autodoc 扩展)的核心思路——遍历模块成员,提取每个可调用对象的签名、文档字符串和源代码,然后格式化为文档。

import inspect import types def generate_api_docs(module): """为模块中的所有公共函数生成 Markdown 文档""" docs = [] docs.append(f"# {module.__name__} API 文档\n") for name, obj in inspect.getmembers(module): if name.startswith('_'): continue if inspect.isfunction(obj): docs.append(f"## {name}\n") sig = inspect.signature(obj) docs.append(f"```python\n{name}{sig}\n```\n") doc = inspect.getdoc(obj) if doc: docs.append(f"{doc}\n") docs.append("") return "\n".join(docs) # 使用示例 print(generate_api_docs(__import__('math')))

7.4 总结与最佳实践

inspect 模块是 Python 标准库中功能最为丰富的工具模块之一,它为开发者提供了从运行时检视代码本身的全套能力。总结其核心价值:第一,调试与诊断——stack() 和 currentframe() 可以精确定位问题代码的位置;第二,元编程基础设施——ismethod、isfunction、isclass 等谓词函数使装饰器、元类和其他元编程模式可以正确处理不同类型的对象;第三,框架与工具构建——signature() 和 getsource() 是构建 Web 框架路由、依赖注入容器、参数校验器和文档生成器的底层基石;第四,学习与探索——getsource 和 getsourcelines 可以在 EDA(探索性数据分析)场景下快速查看库函数的实现细节。

使用 inspect 时需要注意几个性能和安全方面的问题:首先,stack() 和 trace() 的开销不可忽略,生产环境的性能关键路径上应避免高频调用;其次,getsource 依赖于源文件的存在,打包为 .exe 或使用 zipimport 部署的应用中可能无法使用;再次,注意 inspect 的执行安全性——它本质上是读取源文件和执行任意代码的中间地带,在沙箱环境中使用 inspect 需要额外注意。

一句话总结:inspect 是 Python 自省能力的集大成者。能熟练使用 getsource、signature 和 stack 这三个核心函数,就能解决 80% 以上的对象内省需求。结合装饰器和元类模式,inspect 可以构建出非常优雅和强大的框架级工具。