函数内省
除了 __doc__
,函数对象还有很多属性。使用 dir
函数可以探知factorial
具有下述属性:
print(dir(factorial))
['__annotations__', '__call__', '__class__', '__closure__', '__code__',
'__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__',
'__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__',
'__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__',
'__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__',
'__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__',
'__subclasshook__']
其中大多数属性是 Python 对象共有的。本节讨论与把函数视作对象相关的几个属性,先从 __dict__
开始。
与用户定义的常规类一样,函数使用 __dict__
属性存储赋予它的用户属性。这相当于一种基本形式的注解。一般来说,为函数随意赋予属性不是很常见的做法,但是 Django 框架这么做了。参见“The Django adminsite”文档中对short_description
、boolean
和 allow_tags
属性的说明。这篇Django 文档中举了下述示例,把 short_description
属性赋予一个方法,Django 管理后台使用这个方法时,在记录列表中会出现指定的描述文本:
def upper_case_name(obj):
return ("%s %s" % (obj.first_name, obj.last_name)).upper()
upper_case_name.short_description = 'Customer name'
下面重点说明函数专有而用户定义的一般对象没有的属性。计算两个属性集合的差集便能得到函数专有属性列表:
class C: pass
obj = C()
def func(): pass
print(sorted(set(dir(func)) - set(dir(obj))))
['__annotations__', '__call__', '__closure__', '__code__', '__defaults__',
'__get__', '__globals__', '__kwdefaults__', '__name__', '__qualname__']
用户定义的函数的属性:
名称 | 类型 | 说明 |
---|---|---|
__annotations__ | dict | 参数和返回值的注解 |
__call__ | method-wrapper | 实现 () 运算符;即可调用对象协议 |
__closure__ | tuple | 函数闭包,即自由变量的绑定(通常是 None ) |
__code__ | code | 编译成字节码的函数元数据和函数定义体 |
__defaults__ | tuple | 形式参数的默认值 |
__get__ | method-wrapper | 实现只读描述符协议(参见第 20 章) |
__globals__ | dict | 函数所在模块中的全局变量 |
__kwdefaults__ | dict | 仅限关键字形式参数的默认值 |
__name__ | str | 函数名称 |
__qualname__ | str | 函数的限定名称,如 Random.choice ( 参阅PEP3155) |
后面几节会讨论 __defaults__
、__code__
和 __annotations__
属性,IDE和框架使用它们提取关于函数签名的信息。但是,为了深入了解这些属性,我们要先探讨 Python 为声明函数形参和传入实参所提供的强大句法。
从定位参数到仅限关键字参数
Python 最好的特性之一是提供了极为灵活的参数处理机制,而且Python3进一步提供了仅限关键字参数(keyword-only argument)。与之密切相关的是,调用函数时使用 * 和 **“展开”可迭代对象,映射到单个参数。
tag
函数用于生成 HTML 标签;使用名为 cls
的关键字参数传入“class”属性,这是一种变通方法,因为“ class”是 Python的关键字
def tag(name, *content, cls=None, **attrs):
"""生成一个或多个HTML标签"""
if cls is not None:
attrs['class'] = cls
if attrs:
attr_str = ''.join(' %s="%s"' % (attr, value)
for attr, value
in sorted(attrs.items()))
else:
attr_str = ''
if content:
return '\n'.join('<%s%s>%s</%s>' %
(name, attr_str, c, name) for c in content)
else:
return '<%s%s />' % (name, attr_str)
tag 函数的调用方式很多
print(tag('br')) # '<br />'
print(tag('p', 'hello')) # '<p>hello</p>'
print(tag('p', 'hello', 'world'))
# <p>hello</p>
# <p>world</p>
print(tag('p', 'hello', id=33)) # '<p id="33">hello</p>'
print(tag('p', 'hello', 'world', cls='sidebar'))
# <p class="sidebar">hello</p>
# <p class="sidebar">world</p>
print(tag(content='testing', name="img")) # '<img content="testing" />'
my_tag = {'name': 'img', 'title': 'Sunset Boulevard', 'src': 'sunset.jpg', 'cls': 'framed'}
print(tag(**my_tag))
# '<img class="framed" src="sunset.jpg" title="Sunset Boulevard" />'
获取关于参数的信息
HTTP 微框架 Bobo 中有个使用函数内省的好例子。下方代码是对 Bobo教程中“Hello world”应用的改编,说明了内省怎么使用。
import bobo
@bobo.query('/')
def hello(person):
return 'Hello %s!' % person
bobo.query
装饰器把一个普通的函数(如 hello)与框架的请求处理机制集成起来了。装饰器会在第 7 章讨论,这不是这个示例的关键。这里的关键是,Bobo 会内省 hello
函数,发现它需要一个名为 person
的参数,然后从请求中获取那个名称对应的参数,将其传给 hello
函数,因此程序员根本不用触碰请求对象。
安装 Bobo,然后启动开发服务器,执行上例中的脚本(例如,bobo -f hello.py
)。访问 http://localhost:8080/
看到的消息是“Missing form variable person”,HTTP 状态码是 403
。这是因为,Bobo 知道调用 hello
函数必须传入 person
参数,但是在请求中找不到同名参数。
如果请求中缺少函数的参数,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!’:
$ 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
函数;在指定长度附近截断字符串的函数:
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()
审查上述示例中定义的 clip
函数,查看__defaults__
、__code__.co_varnames
和 __code__.co_argcount
的值。
提取关于函数参数的信息
print(clip.__defaults__)
# (80,)
print(clip.__code__) # doctest: +ELLIPSIS
# <code object clip at 0x...>
print(clip.__code__.co_varnames)
# ('text', 'max_len', 'end', 'space_before', 'space_after')
print(clip.__code__.co_argcount)
# 2
可以看出,这种组织信息的方式并不是最便利的。参数名称在__code__.co_varnames
中,不过里面还有函数定义体中创建的局部变量。因此,参数名称是前 N
个字符串,N
的值由__code__.co_argcount
确定。顺便说一下,这里不包含前缀为 *
或**
的变长参数。参数的默认值只能通过它们在 __defaults__
元组中的位置确定,因此要从后向前扫描才能把参数和默认值对应起来。在这个示例中 clip
函数有两个参数,text
和 max_len
,其中一个有默认值,即 80
,因此它必然属于最后一个参数,即 max_len
。这有违常理。
幸好,我们有更好的方式——使用 inspect
模块1来提取函数的签名2:
from inspect import signature
sig = signature(clip)
print(sig) # doctest: +ELLIPSIS
# python3.5后: '(text, max_len=80)'
# python3.5前: <inspect.Signature object at 0x...>
print(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
,但是可能包含 Python3 新的注解句法提供的函数签名元数据。
inspect.Signature
对象有个 bind
方法,它可以把任意个参数绑定到签名中的形参上,所用的规则与实参到形参的匹配方式一样。框架可以使用这个方法在真正调用函数前验证参数。
把之前的tag
函数的签名绑定到一个参数字典上3:
import inspect
sig = inspect.signature(tag)
my_tag = {'name': 'img', 'title': 'Sunset Boulevard',
'src': 'sunset.jpg', 'cls': 'framed'}
bound_args = sig.bind(**my_tag)
print(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)
# raise TypeError(msg) from None
# TypeError: missing a required argument: 'name'
这个示例在 inspect
模块的帮助下,展示了Python数据模型把实参绑定给函数调用中的形参的机制,这与解释器使用的机制相同。
框架和IDE等工具可以使用这些信息验证代码。Python3的另一个特性——函数注解——增进了这些信息的用途,参见下一节。
python进阶书目串烧(一)—— 特殊方法、序列数组、列表推导、生成器表达
python进阶书目串烧(二)—— 元组拆包、具名元组、元组对比列表、切片
python进阶书目串烧(三)—— 序列、排序、列表对比数组
python进阶书目串烧(四)—— 内存视图、NumPy、列表对比双向队列
python进阶书目串烧(五)—— 泛映射类型、字典推导、映射的弹性键查询
python进阶书目串烧(六)—— 字典变种、不可变映射类型、集合推导
python进阶书目串烧(七)—— 字典原理、字典与集合特征对比
python进阶书目串烧(八)—— 内容透视:字符、字节、编解码
python进阶书目串烧(九)—— 内容透视:高阶函数、匿名函数、可调用对象
python进阶书目串烧(十)—— 内容透视:函数内省、函数参数绑定原理
B2中涉及到
inspect
使用的位置:9.7 利用装饰器强制函数上的类型检查、9.11 装饰器为被包装函数增加参数、9.16*args
和**kwargs
的强制参数签名、9.17 在类上强制使用编程规约 ↩︎在 Python 3.5 中,本示例的
sig
的值是:<Signature (text, max_len=80)>
。 ↩︎在 Python 3.5 中,本示例的
bound_args
的值是:<BoundArguments (name='img',cls='framed', attrs={'title': 'Sunset Boulevard', 'src': 'sunset.jpg'})>
。 ↩︎