返回介绍

5.8 获取关于参数的信息

发布于 2024-02-05 21:59:47 字数 5777 浏览 0 评论 0 收藏 0

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 的另一个特性——函数注解——增进了这些信息的用途,参见下一节。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文