先来看个关于HTTP微框架Bobo的例子
import bobo
@bobo.query('/')
def hello(person):
return 'Hello %s!' % person
bobo.query
装饰器把一个普通的函数(如 hello
)与框架的请求处理机制集成起来了。Bobo 会内省 hello
函数,发现它需要一个名为 person
的参数,然后从请求中获取那个名称对应的参数,将其传给 hello
函数
重点是,Bobo 是怎么知道函数需要哪个参数的呢?它又是怎么知道参数有没有默认值呢?
其实函数对象有个 defaults 属性,它的值是一个元组,里面保存着定位参数和关键字参数的默认值。仅限关键字参数的默认值在 kwdefaults 属性中。然而,参数的名称在 code 属性中,它的值是一个 code 对象引用,自身也有很多属性。
下面看一下提取关于函数参数的信息的示例
def tag(name, age=12, *content, cls=None, **attrs):
x = 2
pass
print(tag.__defaults__)
print(tag.__kwdefaults__)
print(tag.__code__.co_varnames)
print(tag.__code__.co_argcount)
"""
(12,)
{'cls': None}
('name', 'age', 'cls', 'content', 'attrs', 'x')
2
"""
参数名称在 __code__.co_varnames
中,不过里面还有函数定义体中创建的局部变量。
参数名称是前 N 个字符串,N 的值由 __code__.co_argcount
确定, 但是不包含前缀为 *
或 **
的变长参数。
参数的默认值只能通过它们在 __defaults__
元组中的位置确定,因此要从后向前扫描才能把参数和默认值对应起来。在这个示例中函数有两个参数(除了变长参数和仅限关键字参数),name
和 age
,其中一个有默认值,即 12,因此它必然属于最后一个参数,即 age。
上面的实例看起来有些别扭,当然有更好的方式可以查看函数参数的信息,使用 inspect
模块
from inspect import signature
sig = signature(tag)
print(str(sig))
for name, param in sig.parameters.items():
print(param.kind, ':', name, '=', param.default)
"""
(name, age=12, *content, cls=None, **attrs)
POSITIONAL_OR_KEYWORD : name = <class 'inspect._empty'>
POSITIONAL_OR_KEYWORD : age = 12
VAR_POSITIONAL : content = <class 'inspect._empty'>
KEYWORD_ONLY : cls = None
VAR_KEYWORD : attrs = <class 'inspect._empty'>
"""
inspect.signature
函数返回一个 inspect.Signature
对象,它有一个 parameters
属性,这是一个有序映射,把参数名和 inspect.Parameter
对象对应起来。各个 Parameter
属性也有自己的属性,例如 name
、default
和 kind
。特殊的 inspect._empty
值表示没有默认值,考虑到 None
是有效的默认值(也经常这么做),而且这么做是合理的。
另外,inspect.Signature
对象有个 bind
方法,它可以把任意个参数绑定到签名中的形参上,所用的规则与实参到形参的匹配方式一样。框架可以使用这个方法在真正调用函数前验证参数。
sig = signature(tag)
my_tag = {'name': 'img', 'title': 'Cookbook', 'cls': 'framed'}
bound_args = sig.bind(**my_tag)
for name, value in bound_args.arguments.items():
print(name, '=', value)
"""
name = img
cls = framed
attrs = {'title': 'Cookbook'}
"""
如果把必须指定的参数 name
从 my_tag
中删除,那么调用 sig.bind(**my_tag)
将会抛出 TypeError
, 抱怨缺少 name
参数。