Python函数参数的自省

 

函数参数的自省

函数自省简介

检索有关参数的信息

肥皂盒:关于Bobo

 

函数自省简介

Python 函数是成熟的对象。因此,它们具有诸如 __doc__ 之类的属性:

>>> def factorial(n):

...     """returns n!"""

...     return 1 if n < 2 else n * factorial(n - 1)

...

>>> factorial.__doc__

'returns n!'

Python 控制台上调用 help(factorial) 将显示函数签名和 __doc__ 文本。

 

函数除了 __doc__ 之外还有许多属性。以下是 dir 函数揭示factorial函数的属性:

>>> dir(factorial)
['__annotations__', '__builtins__', '__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__ 属性来存储分配给它的用户定义的属性。作为注解(annotion)的一种原始形式,这一点非常有用。一般来说,为函数指定任意属性的做法并不常见,但 Django 是使用这种做法的框架之一。例如,请参阅 Django 管理站点文档中描述的 short_descriptionboolean admin_order_field 属性。本例改编自 Django 文档,展示了将 short_description 附加到Module子类的方法中,以确定使用该方法时在 Django 管理用户界面的记录列表中显示该描述:

    def is_published(self, obj):
        return obj.publish_date is not None
    is_published.short_description = 'Is Published?'

 

现在,让我们来关注函数特有的属性,这些属性在通用的 Python 用户定义对象中是找不到的。通过计算两个集合的差值,我们很快就能得到函数特有属性的列表(见示例 1)。

 

示例 1. 列出普通实例中不存在的但是属于函数的属性

>>> class C: pass  # (1)
>>> obj = C()  # (2)
>>> def func(): pass  # (3)
>>> sorted(set(dir(func)) - set(dir(obj))) # (4)
['__annotations__', '__call__', '__closure__', '__code__', '__defaults__',
'__get__', '__globals__', '__kwdefaults__', '__name__', '__qualname__']
>>> 

1)创建用户自定义的裸类。

2)创建一个实例。

3)创建一个裸函数。

4)使用集差法生成一个排序列表,列出存在于函数中但不存在于裸类实例中的属性

 

1 显示了示例 1 列出的属性的摘要。

1:用户自定义函数拥有的属性

Table 1. Attributes of user-defined functions

Name

Type

Description

__annotations__

dict

参数和返回值类型提示

__call__

method

() 运算符的实现;又名可调用对象协议

__closure__

tuple

函数闭包,即绑定了自由变量(通常是 None

__code__

code

函数元数据和函数体编译成字节码

__defaults__

tuple

形参的默认值

__get__

method

只读描述符协议的实现 (参考 [attribute_descriptors])

__globals__

dict

引用定义函数的模块的全局变量

__kwdefaults__

dict

仅关键字形式参数的默认值

__name__

str

函数名称

__qualname__

str

限定函数名称,例如 Random.choice(请参阅 PEP-3155

 

检索有关参数的信息

Bobo HTTP 微框架是函数自省的一个有趣应用。要了解函数内省的实际应用,请参考例 2 Bobo 教程 "Hello world "应用程序的变体。

 

注意:

我提到 Bobo 是因为它自 1997 年以来就率先使用参数自省来减少 Python Web 框架中的样板代码!这种做法现在很常见。 FastAPI 是使用相同想法的现代框架的一个示例。

 

示例 2. Bobo 知道 hello 需要 person 参数,并从 HTTP 请求中检索它

import bobo
 
@bobo.query('/')
def hello(person):
    return f'Hello {person}!'

bobo.query 装饰器将简单的函数(例如 hello)与框架的请求处理机制集成在一起。我们将在 [closures_and_decorators] 中介绍装饰器——这不是本示例的重点。重点是,Bobo 内省了 hello 函数,发现它需要一个名为 person 的参数才能工作,并且它会从请求中检索具有该名称的参数并将其传递给 hello,因此程序员不需要直接地处理请求对象。这也使得单元测试变得更加容易:不需要模拟请求对象来测试 hello 函数。

 

如果您安装 Bobo 并将其开发服务器指向示例 2 中的脚本(例如 bobo -f hello.py),则点击 URL http://localhost:8080/ 将生成缺少表单变量 person” 403 HTTP 代码页。发生这种情况是因为 Bobo 知道调用 hello 需要 person 参数,但在请求中找不到这样的名称。示例 3 是使用curl shell 会话来展示此行为。

 

示例 3. 如果请求中缺少函数参数,Bobo 会发出 403 禁止响应; curl -i HTTP响应标头转储到标准输出

$ curl -i http://localhost:8080/
HTTP/1.0 403 Forbidden
Date: Mon, 31 May 2021 16:34:19 GMT
Server: WSGIServer/0.2 CPython/3.9.5
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。参见示例 4

示例 4. 传递 person 参数则是OK响应

$ curl -i http://localhost:8080/?person=Jim
HTTP/1.0 200 OK
Date: Mon, 31 May 2021 16:35:40 GMT
Server: WSGIServer/0.2 CPython/3.9.5
Content-Type: text/html; charset=UTF-8
Content-Length: 10
 
Hello Jim!

Bobo框架如何知道函数需要哪些参数名称,以及它们是否有默认值?

 

在函数对象中, __defaults__ 属性保存一个包含位置参数和关键字参数的默认值的元组。仅关键字参数的默认值出现在 __kwdefaults__ 中。然而,参数的名称可以在 __code__ 属性中找到,该属性是对具有许多自身属性的code对象的引用。

 

为了演示这些属性的使用,我们将检查例 5 中列出的 clip 函数。clip 函数尝试在空格处中断文本字符串,并尽可能使 len(result) max_len示例代码库 clip.py doctests 演示了该函数的工作原理。在这里,我们更关注函数的签名而非主体。

 

5. 在接近所需长度的位置剪切,缩短字符串。

def clip(text, max_len=80):
    """Return max_len characters clipped at space if possible"""
    text = text.rstrip()
    if len(text) <= max_len or ' ' not in text:
        return text
    end = len(text)
    space_at = text.rfind(' ', 0, max_len + 1)
    if space_at >= 0:
        end = space_at
    else:
        space_at = text.find(' ', max_len)
        if space_at >= 0:
            end = space_at
    return text[:end].rstrip()

示例 6 显示了示例 5 中列出的clip函数的 __defaults____code__.co_varnames __code__.co_argcount 的值。

 

6. 提取函数参数信息

>>> 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_at')
>>> clip.__code__.co_argcount
2

正如你所看到的,这并不是最方便的信息排列方式。参数名出现在__code__.co_varnames 中,但其中也包括在函数体中创建的局部变量的名称。因此,参数名是前 N 个字符串,其中 N __code__.co_argcount 给出,顺便说一下,它不包括任何前缀为 * 的变量参数。缺省值只通过它们在 __defaults__ 元组中的位置来标识,因此要将每个缺省值与相应的参数连接起来,必须从最后一个到第一个进行扫描。在这个示例中,我们有两个参数 text max_len,以及一个默认值 80,因此80必须属于最后一个参数 max_len。这就很尴尬了。

 

幸运的是,有一个更好的方法:inspect 模块。

 

看一下示例 7

示例 7. 提取函数签名

>>> from clip import clip
>>> from inspect import signature
>>> sig = signature(clip)
>>> sig
<Signature (text, max_len=80)>
>>> 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实例都有 namedefault kind 等属性。特殊值 inspect._empty 表示没有默认值的参数,考虑到 None 是一个有效且常作为函数参数的默认值,这一点很有意义。

 

kind 属性包含 _ParameterKind 类中五个可能值之一:

POSITIONAL_OR_KEYWORD

可以作为位置参数或关键字参数传递的参数(大多数 Python 函数参数都是这种类型)。

 

VAR_POSITIONAL

位置参数的元组。(可变位置参数)

 

VAR_KEYWORD

关键字参数的字典。

 

KEYWORD_ONLY

仅关键字参数(Python 3 中的新增功能)。

POSITIONAL_ONLY

仅位置参数; Python 3.8 之前的函数声明语法不支持此语法,但以 C 语言实现的现有函数(如 divmod)为例,这些函数不接受通过关键字传递参数。

 

除了 namedefault kind 之外,inspect.Parameter 对象还有一个annotation属性,该属性通常为 inspect._empty,但可能包含通过 Python 3 中的新注释语法提供的函数签名元数据 - [type_hints_in_def_ch] 中介绍。

 

Inspect.Signature 对象具有一个bind方法,该方法接受任意数量的参数并将它们绑定到签名中的参数,应用将实际参数与形式参数匹配的常用规则。框架可以使用它在实际函数调用之前验证参数。示例 8 展示了如何实现。

 

示例 8. [tagger_ex] 中标签函数的函数签名绑定到参数字典

>>> import inspect
>>> sig = inspect.signature(tag)  (1)
>>> my_tag = {'name': 'img', 'title': 'Sunset Boulevard',
...           'src': 'sunset.jpg', 'class_': 'framed'}
>>> bound_args = sig.bind(**my_tag)  (2)
>>> bound_args
<BoundArguments (name='img', class_='framed',
  attrs={'title': 'Sunset Boulevard', 'src': 'sunset.jpg'})>  (3)
>>> for name, value in bound_args.arguments.items():  (4)
...     print(name, '=', value)
...
name = img
class_ = framed
attrs = {'title': 'Sunset Boulevard', 'src': 'sunset.jpg'}
>>> del my_tag['name']  (5)
>>> bound_args = sig.bind(**my_tag)  (6)
Traceback (most recent call last):
  ...
TypeError: missing a required argument: 'name'

1)从[tagger_ex]中的tag函数获取签名。

2)将参数组成的字典传递给.bind函数

3)生成一个inspect.BoundArguments对象(添加换行符以适合电子书)。

4)迭代bound_args.arguments(一个字典)中的项目,以显示参数的名称和值。

5 my_tag 中删除强制参数name

6调用 sig.bind(**my_tag) 会引发 TypeError,抱怨缺少name参数。

 

这个示例展示了 Python 数据模型如何在 inspection 的帮助下,揭露了解释器用来在函数调用中将参数绑定到形式参数的机制。框架和 IDE 等工具可以使用这些信息来验证代码。

 

警告:

inspection 模块早于 PEP 484-类型提示。在 Python 3.9 中,对类型提示进行正确的运行时检查更为复杂,支持也不完善。

 

 

肥皂盒:关于Bobo

我的 Python 职业生涯开始于Bobo。在尝试 Perl Java 替代方案之后,我在寻找一种面向对象的 Web 应用程序编码方法时发现了它。 1998 年,我在第一个 Python Web 项目中使用了 Bobo,该项目是一个名为 IDG Now IT 新闻门户网站,它是美国媒体公司 International Data Group 的巴西子公司。

 

1997年,Bobo首创了对象发布概念(object publishing concept):从URL直接映射到对象层次结构,无需配置路由。当我看到它的美丽时,我被迷住了。 Bobo 还能根据对用于处理请求的方法或函数签名的分析,自动处理 HTTP 查询。

Bobo 由吉姆-富尔顿(Jim Fulton)创建,他在开发 Zope 框架(ZopePlone CMSSchoolToolERP5 和其他大型 Python 项目的基础)方面发挥了主导作用,因此后来被称为 "Zope 教皇"。吉姆还是 ZODBZope 对象数据库)的创建者,ZODB 是一种事务对象数据库,提供 ACID(原子性、一致性、隔离性和持久性),旨在方便使用 Python

 

BoboZope的内核。当 Zope 1998 年底发布时,我感到非常高兴,因为我想在我的软件开发工作中使用 Bobo,但是使用一种以喜剧团命名的晦涩语言(Python语言)和一个其名称在葡萄牙语是傻瓜的意思的未知的框架在巴西销售项目并不容易(Bobo在葡萄牙语是的意思)。Zope也不为人所知,但至少这个名字是中性的。它还包括ZODB,这让人印象深刻。

 

此后,Jim 从头开始​​重写了 Bobo,以支持 WSGI 和现代 Python(包括 Python 3)。

 

评论

此博客中的热门博文

OAuth 2教程

网格策略

apt-get详细使用