python进阶书目串烧(十)

函数内省

除了 __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_descriptionbooleanallow_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 函数有两个参数,textmax_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 属性也有自己的属性,例如 namedefaultkind。特殊的 inspect._empty 值表示没有默认值,考虑到 None 是有效的默认值(也经常这么做),而且这么做是合理的。

kind 属性的值是 _ParameterKind 类中的 5 个值之一:

  • POSITIONAL_OR_KEYWORD:可以通过定位参数和关键字参数传入的形参(多数 Python 函数的参数属于此类)。
  • VAR_POSITIONAL:定位参数元组。
  • VAR_KEYWORD:关键字参数字典。
  • KEYWORD_ONLY:仅限关键字参数(Python 3 新增)。
  • POSITIONAL_ONLY:仅限定位参数;目前,Python 声明函数的句法不支持,但是有些使用 C 语言实现且不接受关键字参数的函数(如 divmod)支持。

除了 namedefaultkindinspect.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进阶书目串烧(十)—— 内容透视:函数内省、函数参数绑定原理


  1. B2中涉及到inspect使用的位置:9.7 利用装饰器强制函数上的类型检查、9.11 装饰器为被包装函数增加参数、9.16 *args**kwargs 的强制参数签名、9.17 在类上强制使用编程规约 ↩︎

  2. 在 Python 3.5 中,本示例的 sig 的值是:<Signature (text, max_len=80)>↩︎

  3. 在 Python 3.5 中,本示例的 bound_args 的值是:<BoundArguments (name='img',cls='framed', attrs={'title': 'Sunset Boulevard', 'src': 'sunset.jpg'})>↩︎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值