函数签名与inspect模块
深入理解Python函数签名和参数自省
一、概述:什么是函数签名
在Python中,函数签名(Function Signature) 描述了函数的输入输出协议——它定义了函数接受哪些参数、这些参数的类型约束以及函数返回值的类型。Python标准库中的 inspect 模块提供了强大的签名提取和分析工具,其核心是 Signature 对象和 Parameter 对象,二者共同构成了Python函数自省机制的基石。
函数签名在现代Python开发中扮演着至关重要的角色:它是装饰器编写 的基础设施,是依赖注入框架 的底层支撑,也是类型检查器 和文档生成器 的数据来源。深入理解函数签名,能让你写出更加灵活、健壮的Python代码。
核心概念速览:
Signature 对象 :代表函数的完整签名,包含参数列表和返回值注解
Parameter 对象 :代表签名中的单个参数,包含名称、种类、默认值、注解等信息
参数种类(Parameter.kind) :区分参数是位置参数、关键字参数还是可变参数
inspect.signature() :从函数对象中提取签名的标准接口
二、Signature 对象详解
Signature 对象是函数签名的容器,它聚合了函数所有的参数信息以及返回值注解。我们可以通过 inspect.signature() 函数从任意可调用对象中获取其签名。
2.1 获取函数签名
最基本的使用方式如下:
import inspect
def greet (name: str , age: int = 18 , *tags: str , verbose: bool = False ) -> str :
"""向某人问好"""
msg = f"Hello, {name}!"
if verbose:
msg += f" Age: {age}."
return msg
sig = inspect .signature(greet)
print (sig) # (name: str, age: int = 18, *tags: str, verbose: bool = False) -> str
print (type (sig)) #
print (repr (sig)) # str>
2.2 Signature 的核心属性
parameters: OrderedDict[str, Parameter]
一个有序字典,按照参数定义顺序存储所有 Parameter 对象。键是参数名,值是 Parameter 实例。
sig = inspect .signature(greet)
print (list (sig.parameters.keys()))
# ['name', 'age', 'tags', 'verbose']
# 遍历所有参数
for name, param in sig.parameters.items():
print (f"{name}: {param.kind.name}" )
# name: POSITIONAL_OR_KEYWORD
# age: POSITIONAL_OR_KEYWORD
# tags: VAR_POSITIONAL
# verbose: KEYWORD_ONLY
return_annotation: Any
函数的返回值注解。如果没有注解,返回 Signature.empty。
print (sig.return_annotation) #
print (sig.return_annotation is inspect .Signature.empty) # False
# 无注解的情况
def plain_func (x):
return x
sig2 = inspect .signature(plain_func)
print (sig2.return_annotation) #
print (sig2.return_annotation is inspect .Signature.empty) # True
2.3 bind 与 bind_args:动态参数绑定
Signature 提供了两个强大的方法 —— bind() 和 bind_args() —— 用于将实际参数动态绑定到签名的参数上。这是实现依赖注入和参数校验的核心工具。
sig = inspect .signature(greet)
# bind(): 绑定参数,返回 BoundArguments 对象
# 如果参数不匹配,抛出 TypeError
bound = sig.bind("Alice" , 25 , "student" , "urgent" , verbose=True )
print (bound)
#
# 通过 .arguments 获取绑定后的参数字典
for name, value in bound.arguments.items():
print (f"{name} = {value!r}" )
# name = 'Alice'
# age = 25
# tags = ('student', 'urgent')
# verbose = True
# bind_args(): 更宽松的版本,允许多余的关键字参数
# 注意:多余的参数会被收集到 **kwargs 中
try :
sig.bind("Bob" , unknown=42 )
except TypeError as e:
print (e) # got an unexpected keyword argument 'unknown'
# bind_args 允许额外关键字参数
bound2 = sig.bind_args("Bob" , 30 , unknown=42 )
print (bound2.arguments)
# {'name': 'Bob', 'age': 30, 'unknown': 42}
bind 与 bind_args 的核心区别:
bind() :严格匹配签名定义,未知关键字参数会抛出 TypeError,适合用于参数校验
bind_args() :允许额外关键字参数存在,适合用于参数透传和容器适配
二者都返回 BoundArguments 对象,通过 .arguments 属性获取参数字典
三、Parameter 对象详解
Parameter 对象描述函数签名中的一个具体参数。它包含五个核心属性:name、kind、default、annotation 和 empty 哨兵值。
3.1 创建与访问 Parameter
from inspect import Parameter, Signature
# 手动创建一个 Parameter
param = Parameter(
name='username' ,
kind=Parameter.POSITIONAL_OR_KEYWORD,
default=None ,
annotation=str
)
print (param)
# 'username: str = None'
3.2 Parameter 的五个核心属性
属性
类型
说明
示例值
name
str
参数名称
'username'
kind
ParameterKind
参数种类枚举
Parameter.KEYWORD_ONLY
default
Any
参数的默认值
None 或 Parameter.empty
annotation
Any
参数的类型注解
str 或 Parameter.empty
empty
sentinel
表示"未设置"的哨兵值
Parameter.empty
sig = inspect .signature(greet)
for name, param in sig.parameters.items():
print (f"╔══ {name} ══╗" )
print (f" name: {param.name}" )
print (f" kind: {param.kind.name} ({param.kind.value})" )
print (f" default: {param.default!r}" )
print (f" annotation: {param.annotation!r}" )
# 输出结果:
# ╔══ name ══╗
# name: name
# kind: POSITIONAL_OR_KEYWORD (1)
# default:
# annotation:
# ╔══ age ══╗
# name: age
# kind: POSITIONAL_OR_KEYWORD (1)
# default: 18
# annotation:
# ╔══ tags ══╗
# name: tags
# kind: VAR_POSITIONAL (2)
# default:
# annotation:
# ╔══ verbose ══╗
# name: verbose
# kind: KEYWORD_ONLY (3)
# default: False
# annotation:
四、参数种类(Parameter.kind)的深入理解
Python 支持五种参数种类,Parameter.kind 属性用枚举值标识每个参数的类型。理解这五种参数种类是正确解析函数签名的前提。
4.1 五种参数种类对照表
枚举值
数值
含义
语法形式
示例
POSITIONAL_ONLY
0
仅位置参数
在 / 之前
def f(a, b, /):
POSITIONAL_OR_KEYWORD
1
位置或关键字参数
普通参数
def f(a, b):
VAR_POSITIONAL
2
可变位置参数
*args
def f(*args):
KEYWORD_ONLY
3
仅关键字参数
在 * 或 *args 之后
def f(*, key):
VAR_KEYWORD
4
可变关键字参数
**kwargs
def f(**kw):
4.2 完整示例:展示所有参数种类
def demo (
a, # POSITIONAL_ONLY -> 在 / 之前
b, # POSITIONAL_ONLY
/,
c, # POSITIONAL_OR_KEYWORD
d=10 , # POSITIONAL_OR_KEYWORD
*e, # VAR_POSITIONAL
f, # KEYWORD_ONLY
g="hello" , # KEYWORD_ONLY
**h # VAR_KEYWORD
):
pass
sig = inspect .signature(demo)
for name, p in sig.parameters.items():
print (f"{name:6s} -> {p.kind.name:25s} (value={p.kind.value})" )
# 输出:
# a -> POSITIONAL_ONLY (value=0)
# b -> POSITIONAL_ONLY (value=0)
# c -> POSITIONAL_OR_KEYWORD (value=1)
# d -> POSITIONAL_OR_KEYWORD (value=1)
# e -> VAR_POSITIONAL (value=2)
# f -> KEYWORD_ONLY (value=3)
# g -> KEYWORD_ONLY (value=3)
# h -> VAR_KEYWORD (value=4)
参数种类的实用判断:
利用 Parameter.kind 的枚举比较,可以精确识别参数类型:
# 判断参数是否只能通过位置传入
def is_positional_only (param: Parameter) -> bool :
return param.kind == Parameter.POSITIONAL_ONLY
# 判断是否为可变参数
def is_variadic (param: Parameter) -> bool :
return param.kind in (Parameter.VAR_POSITIONAL, Parameter.VAR_KEYWORD)
# 判断是否需要默认值(没有默认值的参数是必须传的)
def is_required (param: Parameter) -> bool :
return param.default is Parameter.empty and not is_variadic(param)
五、利用签名处理装饰器中的函数参数
编写装饰器时,保留被装饰函数的签名是一个常见但容易被忽视的需求。使用 functools.wraps 只能保留基本的函数元信息(如 __name__、__doc__),但无法保留签名。下面通过几个典型案例展示如何用 inspect 模块优雅地处理装饰器签名问题。
5.1 基础案例:参数校验装饰器
import inspect
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 Parameter.empty:
expected_type = param.annotation
if not isinstance (value, expected_type):
raise TypeError (
f"参数 '{name}' 期望类型 {expected_type.__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 )) # 8
print (add("3" , 5 )) # TypeError: 参数 'x' 期望类型 int, 实际得到 str
5.2 进阶案例:保留签名信息的日志装饰器
import inspect
import logging
from functools import wraps
logger = logging.getLogger(__name__)
def logged (func):
"""装饰器:记录函数调用的参数名称和值"""
sig = inspect.signature(func)
@wraps (func)
def wrapper (*args, **kwargs):
bound = sig.bind(*args, **kwargs)
bound.apply_defaults()
# 构建有意义的日志信息:显示参数名=值对
args_repr = ", " .join(
f"{name}={value!r}"
for name, value in bound.arguments.items()
)
logger.debug(f"调用 {func.__name__}({args_repr})" )
return func(*args, **kwargs)
return wrapper
@logged
def process_order (
order_id: int ,
items: list ,
*,
discount: float = 0.0 ,
express: bool = False
) -> dict :
return {"order_id" : order_id, "total" : len (items) * (1 - discount)}
# 调用时,日志输出:
# 调用 process_order(order_id=42, items=['a','b'], discount=0.1, express=False)
result = process_order(42 , ["a" , "b" ], discount=0.1 )
5.3 高阶:运行时参数注入
import inspect
from functools import wraps
def inject_context (context_provider):
"""装饰器工厂:从函数签名中识别需要注入的参数,自动注入上下文"""
def decorator (func):
sig = inspect.signature(func)
ctx_params = {
name: param
for name, param in sig.parameters.items()
if param.annotation is not Parameter.empty
and getattr (param.annotation, '__injectable__' , False )
}
@wraps (func)
def wrapper (*args, **kwargs):
context = context_provider()
# 只注入那些未由调用者显式提供的参数
for name in ctx_params:
if name not in kwargs:
kwargs[name] = getattr (context, name, None )
return func(*args, **kwargs)
return wrapper
return decorator
六、签名在依赖注入框架中的应用
依赖注入(Dependency Injection, DI)是现代框架中管理组件依赖关系的核心技术。函数签名是DI框架的"眼睛"——框架通过签名来"看"清一个函数或类需要哪些依赖,以及这些依赖的类型是什么。
6.1 一个迷你依赖注入容器
import inspect
from inspect import Parameter
class Container :
"""一个基于函数签名的迷你依赖注入容器"""
def __init__ (self):
self._providers = {} # type -> callable
def register (self, abstract_type, provider):
"""注册类型及其提供者"""
self._providers[abstract_type] = provider
def resolve (self, func):
"""解析函数的依赖,返回注入后的可调用对象"""
sig = inspect.signature(func)
def wrapper (*args, **kwargs):
# 绑定用户显式传入的参数
bound = sig.bind_partial(*args, **kwargs)
# 检查缺失的参数,通过签名进行依赖注入
for name, param in sig.parameters.items():
if name not in bound.arguments:
annotation = param.annotation
if annotation is not Parameter.empty and annotation in self._providers:
# 自动注入依赖
provider = self._providers[annotation]
if callable (provider):
# 提供者本身可能也需要依赖注入
kwargs[name] = self.resolve(provider)()
else :
kwargs[name] = provider
return func(*args, **kwargs)
return wrapper
# 使用示例
class Database :
def query (self, sql):
return f"结果: {sql}"
class UserService :
def __init__ (self, db: Database):
self.db = db
def get_user (self, user_id: int ):
return self.db.query(f"SELECT * FROM users WHERE id={user_id}" )
container = Container()
container.register(Database, Database())
container.register(UserService, UserService)
# 依赖自动注入
@container.resolve
def handle_request (service: UserService, user_id: int ):
return service.get_user(user_id)
print (handle_request(user_id=42 ))
# 结果: SELECT * FROM users WHERE id=42
这段代码展示了DI框架的核心模式:通过 inspect.signature(func) 获取函数签名,利用 bind_partial() 分离用户已传入的参数和缺失的参数,再根据 param.annotation 查找已注册的提供者,自动完成依赖注入。许多工业级框架(如 FastAPI、Flask-Injector、dependency-injector)的底层原理与此类似。
DI框架中的签名应用模式:
类型匹配 :通过 param.annotation 匹配注册的依赖类型
默认值检测 :param.default is Parameter.empty 判断依赖是否可选
参数名注入 :某些框架(如 AngularJS)也通过参数名进行注入
递归解析 :依赖的提供者本身可能也有依赖,需要递归签名解析
生命周期管理 :通过参数注解区分单例、工厂、瞬态等作用域
七、函数的注解获取
Python 函数注解(Function Annotations)是附加在参数和返回值上的元数据。除了通过 inspect.signature 获取外,还可以通过 __annotations__ 属性直接访问。
7.1 __annotations__ 属性
def process (
name: str ,
count: int = 1 ,
tags: list [str ] = None
) -> list [dict ]:
"""处理数据并返回结果列表"""
return [{"name" : name, "count" : count, "tags" : tags}]
# 方式1: 通过 __annotations__ 字典
print (process.__annotations__)
# {'name': , 'count': , 'tags': list[str], 'return': list[dict]}
# 方式2: 通过 inspect.signature (更推荐:包含更多上下文)
sig = inspect .signature(process)
for name, param in sig.parameters.items():
print (f"{name}: annotation={param.annotation!r}" )
print (f"return: annotation={sig.return_annotation!r}" )
# 注意区别:
# 1. __annotations__ 返回原始字典,参数名是字符串键
# 2. inspect.signature 返回 Signature 对象,可以区分参数种类、默认值等
# 3. __annotations__ 不包含 "无注解" 信息,而 signature 用 Parameter.empty 标识
# 4. signature 會正确处理继承和装饰器链中的注解
7.2 获取嵌套注解(PEP 593 -- Annotated)
from typing import Annotated, get_type_hints
class Validator :
def __init__ (self, min_value: int , max_value: int ):
self.min_value = min_value
self.max_value = max_value
def set_temperature (
value: Annotated[float , Validator(-273.15 , 10000 )]
) -> None :
pass
# 使用 get_type_hints 获取完整注解信息
hints = get_type_hints (set_temperature, include_extras =True )
print (hints)
# {'value': typing.Annotated[float, Validator], 'return': None}
# 提取 Annotated 中的元数据
annotated_type = hints['value' ]
print (getattr (annotated_type, '__metadata__' , []))
# [Validator]
八、__signature__ 自定义属性
Python 允许通过设置 __signature__ 属性来显式覆盖一个可调用对象的签名。这是实现"伪函数"和"参数适配"的核心技术。
__signature__ 的作用机制:
当 inspect.signature() 检测到一个可调用对象时,它会优先检查该对象是否定义了 __signature__ 属性。如果有,则直接返回该属性值,而不进行自动推导。这让我们可以"伪造"或"覆盖"一个函数的签名信息。
8.1 基本用法
import inspect
from inspect import Parameter, Signature
# 创建一个"看起来"需要两个字符串参数的函数
def hidden_func (*args, **kwargs):
"""实际实现:处理任意参数"""
return args, kwargs
# 伪造签名:让外界认为它接受 (username: str, password: str) -> bool
hidden_func.__signature__ = Signature(
parameters=[
Parameter('username' , Parameter.POSITIONAL_OR_KEYWORD, annotation=str ),
Parameter('password' , Parameter.POSITIONAL_OR_KEYWORD, annotation=str ),
],
return_annotation=bool
)
sig = inspect.signature(hidden_func)
print (sig)
# (username: str, password: str) -> bool
# inspect 会被 __signature__ 欺骗
print (sig.parameters['username' ].annotation) #
print (sig.return_annotation) #
8.2 实际应用:参数适配器
import inspect
from inspect import Parameter, Signature
class FunctionAdapter :
"""将旧版函数接口适配为新版签名的适配器"""
def __init__ (self, func, param_mapping: dict ):
self._func = func
self._mapping = param_mapping
# 根据映射关系生成新的签名
original_sig = inspect.signature(func)
new_params = []
for old_name, new_name in param_mapping.items():
original_param = original_sig.parameters[old_name]
new_params.append(
Parameter(
name=new_name,
kind=original_param.kind,
default=original_param.default,
annotation=original_param.annotation
)
)
for name, param in original_sig.parameters.items():
if name not in param_mapping:
new_params.append(param)
self.__signature__ = Signature(
parameters=new_params,
return_annotation=original_sig.return_annotation
)
def __call__ (self, **kwargs):
# 将新参数名映射回旧参数名
reverse_map = {v: k for k, v in self._mapping.items()}
mapped = {}
for name, value in kwargs.items():
old_name = reverse_map.get(name, name)
mapped[old_name] = value
return self._func(**mapped)
# 使用示例:旧版 API 使用 create_user,新版改为 register_user
def create_user (name, email, age):
return f"创建用户: {name}, {email}, {age}"
adapter = FunctionAdapter(create_user, param_mapping={"name" : "username" })
print (inspect.signature(adapter))
# (username, email, age)
result = adapter(username="Alice" , email="alice@example.com" , age=30 )
print (result)
# 创建用户: Alice, alice@example.com, 30
__signature__ 的典型应用场景:
API 兼容层 :重构函数签名后保持对调用方的透明兼容
测试替身 :mock/stub 对象暴露与被替身一致的签名
框架集成 :让自定义的可调用对象能被框架的签名解析器正确识别
代码生成 :动态生成的函数需要正确的签名信息
CLI 工具 :根据自定义签名自动生成命令行参数解析器
九、inspect 模块的其他签名相关工具
9.1 getfullargspec:传统方式的参数检查
import inspect
def sample (a, b=10 , *args, c, **kwargs):
pass
# getfullargspec 返回一个 FullArgSpec 对象
spec = inspect.getfullargspec(sample)
print (f"args: {spec.args}" ) # ['a', 'b']
print (f"varargs: {spec.varargs}" ) # 'args'
print (f"varkw: {spec.varkw}" ) # 'kwargs'
print (f"defaults: {spec.defaults}" ) # (10,)
print (f"kwonlyargs: {spec.kwonlyargs}" ) # ['c']
print (f"annotations:{spec.annotations}" )# {}
# 注:getfullargspec 在新代码中已被 inspect.signature 取代
# Signature 对象提供了更统一、更强大的接口
9.2 getsource 与 getsourcelines
# 获取函数的源代码
print (inspect.getsource(greet))
# def greet(name: str, age: int = 18, *tags: str, verbose: bool = False) -> str:
# """向某人问好"""
# msg = f"Hello, {name}!"
# ...
# 获取源代码的行号和文件路径
lines, line_no = inspect.getsourcelines(greet)
print (f"起始行: {line_no}" )
print (f"文件: {inspect.getfile(greet)}" )
十、最佳实践与常见陷阱
最佳实践
优先使用 inspect.signature :相比 getfullargspec,Signature/Parameter API 更统一、类型信息更完整,且支持 __signature__ 自定义
总是调用 apply_defaults() :在使用 bind() 后调用 apply_defaults() 可以填充默认值,避免后续判断
使用 bind_partial() 进行选择性绑定 :在需要部分绑定参数时(如依赖注入),bind_partial 不会因缺少数值而报错
处理 C 扩展函数 :部分用 C 实现的內建函数没有可提取的签名,需要 try/except 捕获 ValueError
利用 Parameter.empty 进行判断 :判断是否有注解或默认值时,始终使用 is Parameter.empty,不要用 is None
常见陷阱
装饰器不保留签名 :仅用 @wraps 不够,如果是复杂的参数变换,需要用 __signature__ 显式更新或使用 makefun、decorator 等第三方库
类型注解只是元数据 :Python 不会强制检查类型注解,即使注解声明为 int,传入字符串也不会报错
泛型注解的提取 :对于 list[int]、dict[str, Any] 等泛型,需要额外调用 typing.get_origin() 和 typing.get_args() 来分解
get_type_hints() 的字符串解析 :对于包含前向引用(字符串注解)的函数,需要用 typing.get_type_hints() 替代直接访问 __annotations__
性能考虑 :inspect.signature() 有缓存机制,但在热路径中频繁调用仍可能造成性能问题,建议在装饰器工厂中预先提取签名
十一、核心要点总结
Signature 对象 :通过 inspect.signature(func) 获取,包含 parameters(有序参数字典)和 return_annotation(返回值注解),bind() 和 bind_args() 实现动态参数绑定
Parameter 对象 :描述单个参数,核心属性 name(名称)、kind(种类枚举)、default(默认值)、annotation(类型注解)、empty(哨兵值)
五种参数种类 :POSITIONAL_ONLY(仅位置)、POSITIONAL_OR_KEYWORD(位置或关键字)、VAR_POSITIONAL(可变位置)、KEYWORD_ONLY(仅关键字)、VAR_KEYWORD(可变关键字),通过 /、*args、*、**kwargs 语法声明
装饰器签名处理 :使用 sig.bind() 将调用参数绑定到签名,实现参数校验、日志记录和上下文注入
依赖注入框架 :通过签名识别函数所需的依赖类型(annotation),利用 bind_partial() 分离用户参数和待注入参数,实现自动化依赖管理
__signature__ 自定义 :通过设置 func.__signature__ = Signature(...) 显式覆盖签名信息,用于 API 兼容、测试替身和框架集成
注解获取 :__annotations__ 属性提供原始字典,inspect.signature 提供结构化解析,get_type_hints 处理前向引用和泛型
本笔记为 Python 进阶编程学习资料,仅供个人学习使用
本学习笔记为本人学习资料,不得转载
免责声明: 本文档中的代码示例仅在 Python 3.10+ 环境下测试,部分特性(如 Annotated)需 Python 3.9+。请根据实际环境验证代码的正确性。