HTTP 微框架 Bobo 中有个使用函数内省的好例子。示例 5-12 是对 Bobo 教程中“Hello world”应用的改编,说明了内省怎么使用。
示例 5-12 Bobo 知道
hello需要person参数,并且从 HTTP 请求中获取它
import bobo
@bobo.query('/')
def hello(person):
return 'Hello %s!' % person
bobo.query 装饰器把一个普通的函数(如 hello)与框架的请求处理机制集成起来了。装饰器会在第 7 章讨论,这不是这个示例的关键。这里的关键是,Bobo 会内省 hello 函数,发现它需要一个名为 person 的参数,然后从请求中获取那个名称对应的参数,将其传给 hello 函数,因此程序员根本不用触碰请求对象。
安装 Bobo,然后启动开发服务器,执行示例 5-12 中的脚本(例如,bobo -f hello.py)。访问 http://localhost:8080/ 看到的消息是“Missing form variable person”,HTTP 状态码是 403。这是因为,Bobo 知道调用 hello 函数必须传入 person 参数,但是在请求中找不到同名参数。示例 5-13 在 shell 会话中使用 curl 展示了这个行为。
示例 5-13 如果请求中缺少函数的参数,Bobo 返回 403 forbidden 响应;
curl -i的作用是把首部转储到标准输出
$ curl -i http://localhost:8080/
HTTP/1.0 403 Forbidden
Date: Thu, 21 Aug 2014 21:39:44 GMT
Server: WSGIServer/0.2 CPython/3.4.1
Content-Type: text/html; charset=UTF-8
Content-Length: 103
<html>
<head><title>Missing parameter</title></head>
<body>Missing form variable person</body>
</html>
然而,如果访问 http://localhost:8080/?person=Jim,响应会变成字符串 'Hello Jim!',如示例 5-14 所示。
示例 5-14 传入所需的
person参数才能得到OK响应
$ curl -i http://localhost:8080/?person=Jim
HTTP/1.0 200 OK
Date: Thu, 21 Aug 2014 21:42:32 GMT
Server: WSGIServer/0.2 CPython/3.4.1
Content-Type: text/html; charset=UTF-8
Content-Length: 10
Hello Jim!
Bobo 是怎么知道函数需要哪个参数的呢?它又是怎么知道参数有没有默认值呢?
函数对象有个 __defaults__ 属性,它的值是一个元组,里面保存着定位参数和关键字参数的默认值。仅限关键字参数的默认值在 __kwdefaults__ 属性中。然而,参数的名称在 __code__ 属性中,它的值是一个 code 对象引用,自身也有很多属性。
为了说明这些属性的用途,下面在 clip.py 模块中定义 clip 函数,如示例 5-15 所示,然后再审查它。
示例 5-15 在指定长度附近截断字符串的函数
def clip(text, max_len=80):
"""在max_len前面或后面的第一个空格处截断文本
"""
end = None
if len(text) > max_len:
space_before = text.rfind(' ', 0, max_len)
if space_before >= 0:
end = space_before
else:
space_after = text.rfind(' ', max_len)
if space_after >= 0:
end = space_after
if end is None: # 没找到空格
end = len(text)
return text[:end].rstrip()
示例 5-16 审查示例 5-15 中定义的 clip 函数,查看 __defaults__、__code__.co_varnames 和 __code__.co_argcount 的值。
示例 5-16 提取关于函数参数的信息
>>> from clip import clip
>>> clip.__defaults__
(80,)
>>> clip.__code__ # doctest: +ELLIPSIS
<code object clip at 0x...>
>>> clip.__code__.co_varnames
('text', 'max_len', 'end', 'space_before', 'space_after')
>>> clip.__code__.co_argcount
2
可以看出,这种组织信息的方式并不是最便利的。参数名称在 __code__.co_varnames 中,不过里面还有函数定义体中创建的局部变量。因此,参数名称是前 N 个字符串,N 的值由 __code__.co_argcount 确定。顺便说一下,这里不包含前缀为 * 或 ** 的变长参数。参数的默认值只能通过它们在 __defaults__ 元组中的位置确定,因此要从后向前扫描才能把参数和默认值对应起来。在这个示例中 clip 函数有两个参数,text 和 max_len,其中一个有默认值,即 80,因此它必然属于最后一个参数,即 max_len。这有违常理。
幸好,我们有更好的方式——使用 inspect 模块。
下面来看一下示例 5-17。
示例 5-17 提取函数的签名 2
2在 Python 3.5 中,本示例的 sig 的值是:<Signature (text, max_len=80)>。——编者注
>>> from clip import clip >>> from inspect import signature >>> sig = signature(clip) >>> sig # doctest: +ELLIPSIS <inspect.Signature object at 0x...> >>> str(sig) '(text, max_len=80)' >>> for name, param in sig.parameters.items(): ... print(param.kind, ':', name, '=', param.default) ... POSITIONAL_OR_KEYWORD : text = <class 'inspect._empty'> POSITIONAL_OR_KEYWORD : max_len = 80
这样就好多了。inspect.signature 函数返回一个 inspect.Signature 对象,它有一个 parameters 属性,这是一个有序映射,把参数名和 inspect.Parameter 对象对应起来。各个 Parameter 属性也有自己的属性,例如 name、default 和 kind。特殊的 inspect._empty 值表示没有默认值,考虑到 None 是有效的默认值(也经常这么做),而且这么做是合理的。
kind 属性的值是 _ParameterKind 类中的 5 个值之一,列举如下。
POSITIONAL_OR_KEYWORD
可以通过定位参数和关键字参数传入的形参(多数 Python 函数的参数属于此类)。
VAR_POSITIONAL
定位参数元组。
VAR_KEYWORD
关键字参数字典。
KEYWORD_ONLY
仅限关键字参数(Python 3 新增)。
POSITIONAL_ONLY
仅限定位参数;目前,Python 声明函数的句法不支持,但是有些使用 C 语言实现且不接受关键字参数的函数(如 divmod)支持。
除了 name、default 和 kind,inspect.Parameter 对象还有一个 annotation(注解)属性,它的值通常是 inspect._empty,但是可能包含 Python 3 新的注解句法提供的函数签名元数据(注解在下一节讨论)。
inspect.Signature 对象有个 bind 方法,它可以把任意个参数绑定到签名中的形参上,所用的规则与实参到形参的匹配方式一样。框架可以使用这个方法在真正调用函数前验证参数,如示例 5-18 所示。
示例 5-18 把
tag函数(见示例 5-10)的签名绑定到一个参数字典上 3
3在 Python 3.5 中,本示例的 bound_args 的值是:<BoundArguments (name='img', cls='framed', attrs={'title': 'Sunset Boulevard', 'src': 'sunset.jpg'})>。——编者注
>>> import inspect
>>> sig = inspect.signature(tag) ➊
>>> my_tag = {'name': 'img', 'title': 'Sunset Boulevard',
... 'src': 'sunset.jpg', 'cls': 'framed'}
>>> bound_args = sig.bind(**my_tag) ➋
>>> bound_args
<inspect.BoundArguments object at 0x...> ➌
>>> for name, value in bound_args.arguments.items(): ➍
... print(name, '=', value)
...
name = img
cls = framed
attrs = {'title': 'Sunset Boulevard', 'src': 'sunset.jpg'}
>>> del my_tag['name'] ➎
>>> bound_args = sig.bind(**my_tag) ➏
Traceback (most recent call last):
...
TypeError: 'name' parameter lacking default value
❶ 获取 tag 函数(见示例 5-10)的签名。
❷ 把一个字典参数传给 .bind() 方法。
❸ 得到一个 inspect.BoundArguments 对象。
❹ 迭代 bound_args.arguments(一个 OrderedDict 对象)中的元素,显示参数的名称和值。
❺ 把必须指定的参数 name 从 my_tag 中删除。
❻ 调用 sig.bind(**my_tag),抛出 TypeError,抱怨缺少 name 参数。
这个示例在 inspect 模块的帮助下,展示了 Python 数据模型把实参绑定给函数调用中的形参的机制,这与解释器使用的机制相同。
框架和 IDE 等工具可以使用这些信息验证代码。Python 3 的另一个特性——函数注解——增进了这些信息的用途,参见下一节。