Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 436 – Argument Clinic DSL

作者:
Larry Hastings <larry at hastings.org>
讨论至:
Python-Dev 列表
状态:
最终版
类型:
标准跟踪
创建日期:
2013年2月22日
Python 版本:
3.4

目录

摘要

本文档提出了“Argument Clinic”,一种 DSL,用于在 CPython 实现中方便内置函数的参数处理。

基本原理和目标

Python 的主要实现“CPython”是用 Python 和 C 混合编写的。CPython 的一个实现细节是所谓的“内置”函数——可供 Python 程序使用但用 C 编写的函数。当 Python 程序调用内置函数并传入参数时,这些参数必须从 Python 值转换为 C 值。此过程称为“解析参数”。

截至 CPython 3.3,内置函数几乎总是使用以下两个函数之一来解析其参数:原始的 PyArg_ParseTuple()[1] 和更现代的 PyArg_ParseTupleAndKeywords()[2] 前者只处理位置参数;后者也支持关键字和仅限关键字的参数,并且在新代码中更受青睐。

使用任一函数,调用者在“格式字符串”中指定解析参数的转换:[3] 每个参数对应一个“格式单元”,一个简短的字符序列,告诉解析函数接受哪些 Python 类型以及如何将它们转换为该参数的适当 C 值。

PyArg_ParseTuple() 在最初构思时是合理的。当时只有大约十几个这样的“格式单元”;每个都不同,易于理解和记忆。但多年来,PyArg_Parse 接口已经以多种方式进行了扩展。现代 API 很复杂,以至于使用起来有些痛苦。考虑一下

  • 现在有四十种不同的“格式单元”;少数甚至有三个字符长。这使得程序员很难理解格式字符串的含义——甚至可能难以解析它——而不必不断地与文档进行交叉索引。
  • 格式字符串中还可能埋藏着六个元格式单元。(它们是:"()|$:;"。)
  • 添加的格式单元越多,实现者为格式单元选择易于使用的助记符的可能性就越小,因为所选择的字符可能已经被使用。换句话说,格式单元越多,格式单元就越晦涩难懂。
  • 有几个格式单元与其他格式单元几乎相同,只有细微的差别。这使得理解格式字符串的确切语义更加困难,并且可能难以弄清楚您到底想要哪个格式单元。
  • 文档字符串被指定为静态 C 字符串,由于它必须遵守 C 字符串引用规则,因此阅读和编辑起来有点麻烦。
  • 使用 PyArg_ParseTupleAndKeywords() 向函数添加新参数时,需要在代码中修改六个不同的地方:[4]
    • 声明用于存储参数的变量。
    • PyArg_ParseTupleAndKeywords() 的正确位置传入该变量的指针,同时以正确的顺序传入任何“长度”或“转换器”参数。
    • 在传入 PyArg_ParseTupleAndKeywords() 的“关键字”数组的正确位置添加参数名称。
    • 在格式字符串的正确位置添加格式单元。
    • 在文档字符串的原型中添加参数。
    • 在文档字符串中记录参数。
  • 目前没有内置函数提供其“签名”信息(参见 inspect.getfullargspecinspect.Signature)的机制。使用类似于现有 PyArg_Parse 函数的机制添加此信息将需要我们再次重复自己。

Argument Clinic 的目标是用一种没有这些缺点的机制来取代这个 API

  • 您只需指定每个参数一次。
  • 有关参数的所有信息都集中在一个地方。
  • 对于每个参数,您指定一个转换函数;Argument Clinic 为您处理从 Python 值到 C 值的转换。
  • Argument Clinic 还允许使用参数化转换函数对参数处理行为进行微调。
  • 文档字符串以纯文本编写。函数文档字符串是必需的;鼓励使用每个参数的文档字符串。
  • 从这一点出发,Argument Clinic 为您生成 CPython 内部所需的所有单调、重复的代码和数据结构。一旦您指定了接口,下一步就是简单地使用原生 C 类型编写您的实现。参数解析的每个细节都为您处理。

Argument Clinic 被实现为一个预处理器。它的工作流程直接从 Ned Batchelder 的 [Cog] 中汲取灵感。要使用 Clinic,请在 C 源代码中添加一个以特殊文本字符串开头和结尾的块注释,然后对文件运行 Clinic。Clinic 将找到块注释,处理其内容,并将输出写回到注释正下方的 C 源文件中。目的是 Clinic 的输出成为您源代码的一部分;它被签入修订控制,并随源代码包一起分发。这意味着 Python 仍然可以随时构建。它确实使开发稍微复杂化;为了添加新函数,或修改使用 Clinic 的现有函数的参数或文档,您需要一个工作的 Python 3 解释器。

Argument Clinic 的未来目标包括

  • 为内置函数提供签名信息,
  • 使 Python 的替代实现能够创建自动化库兼容性测试,以及
  • 通过改进生成的代码来加快参数解析。

DSL 语法总结

Argument Clinic DSL 被指定为嵌入在 C 文件中的注释,如下所示。“示例”列在右侧显示了 Argument Clinic DSL 的示例输入,而“部分”列在左侧依次指定了每行代表什么。

Argument Clinic 的 DSL 语法模仿了 Python 的 def 语句,这使得 Python 核心开发人员对其有些熟悉。

+-----------------------+-----------------------------------------------------------------+
| Section               | Example                                                         |
+-----------------------+-----------------------------------------------------------------+
| Clinic DSL start      | /*[clinic]                                                      |
| Module declaration    | module module_name                                              |
| Class declaration     | class module_name.class_name                                    |
| Function declaration  | module_name.function_name  -> return_annotation                 |
| Parameter declaration |       name : converter(param=value)                             |
| Parameter docstring   |           Lorem ipsum dolor sit amet, consectetur               |
|                       |           adipisicing elit, sed do eiusmod tempor               |
| Function docstring    | Lorem ipsum dolor sit amet, consectetur adipisicing             |
|                       | elit, sed do eiusmod tempor incididunt ut labore et             |
| Clinic DSL end        | [clinic]*/                                                      |
| Clinic output         | ...                                                             |
| Clinic output end     | /*[clinic end output:<checksum>]*/                              |
+-----------------------+-----------------------------------------------------------------+

为了展示所提议的 DSL 语法的一些特点,这里有一些示例 Clinic 代码块。第一个代码块反映了通常首选的样式,包括参数之间的空行和每个参数的文档字符串。它还包括在本地创建的用户定义转换器 (path_t)

/*[clinic]
os.stat as os_stat_fn -> stat result

   path: path_t(allow_fd=1)
       Path to be examined; can be string, bytes, or open-file-descriptor int.

   *

   dir_fd: OS_STAT_DIR_FD_CONVERTER = DEFAULT_DIR_FD
       If not None, it should be a file descriptor open to a directory,
       and path should be a relative string; path will then be relative to
       that directory.

   follow_symlinks: bool = True
       If False, and the last element of the path is a symbolic link,
       stat will examine the symbolic link itself instead of the file
       the link points to.

Perform a stat system call on the given path.

{parameters}

dir_fd and follow_symlinks may not be implemented
  on your platform.  If they are unavailable, using them will raise a
  NotImplementedError.

It's an error to use dir_fd or follow_symlinks when specifying path as
  an open file descriptor.

[clinic]*/

第二个示例显示了一个最小的 Clinic 代码块,省略了所有参数文档字符串和非重要空行

/*[clinic]
os.access
   path: path
   mode: int
   *
   dir_fd: OS_ACCESS_DIR_FD_CONVERTER = 1
   effective_ids: bool = False
   follow_symlinks: bool = True
Use the real uid/gid to test for access to a path.
Returns True if granted, False otherwise.

{parameters}

dir_fd, effective_ids, and follow_symlinks may not be implemented
  on your platform.  If they are unavailable, using them will raise a
  NotImplementedError.

Note that most operations will use the effective uid/gid, therefore this
  routine can be used in a suid/sgid environment to test if the invoking user
  has the specified access to the path.

[clinic]*/

最后一个示例显示了一个处理可选参数组的 Clinic 代码块,包括左侧的参数

/*[clinic]
curses.window.addch

   [
   y: int
     Y-coordinate.

   x: int
     X-coordinate.
   ]

   ch: char
     Character to add.

   [
   attr: long
     Attributes for the character.
   ]

   /

Paint character ch at (y, x) with attributes attr,
overwriting any character previously painter at that location.
By default, the character position and attributes are the
current settings for the window object.
[clinic]*/

Argument Clinic DSL 的一般行为

所有行都支持 # 作为行注释分隔符,除了文档字符串。空行总是被忽略。

像 Python 本身一样,前导空格在 Argument Clinic DSL 中是重要的。“函数”部分的第一行是函数声明。函数声明下方的缩进行声明参数,每行一个;下方缩进更深的行是每个参数的文档字符串。最后,第一行缩进回到第 0 列结束参数声明并开始函数文档字符串。

参数文档字符串是可选的;函数文档字符串不是。不指定参数的函数可以简单地指定函数声明,然后是文档字符串。

模块和类声明

当 C 文件实现模块或类时,应向 Clinic 声明。语法很简单

module module_name

class module_name.class_name

(请注意,这些实际上不是特殊语法;它们是作为指令实现的。)

模块名或类名应始终是来自顶级模块的完整点分路径。支持嵌套模块和类。

函数声明

函数声明的完整形式如下

dotted.name [ as legal_c_id ] [ -> return_annotation ]

点分名称应为函数的完整名称,从最高级包开始(例如“os.stat”或“curses.window.addch”)。

“as legal_c_id”语法是可选的。Argument Clinic 使用函数的名称来创建生成的 C 函数的名称。在某些情况下,生成的名称可能与 C 程序命名空间中的其他全局名称冲突。“as legal_c_id”语法允许您用自己的名称覆盖生成的名称;将“legal_c_id”替换为任何合法的 C 标识符。如果省略,则“as”关键字也必须省略。

返回注释也是可选的。如果省略,则箭头(“->”)也必须省略。如果指定,返回注释的值必须与 ast.literal_eval 兼容,并且它被解释为 返回转换器

参数声明

参数声明行的完整形式如下

name: converter [ (parameter=value [, parameter2=value2]) ] [ = default]

“名称”必须是合法的 C 标识符。名称和冒号之间允许有空格(尽管这不是首选样式)。冒号和转换器之间允许(并鼓励)有空格。

“转换器”是 Argument Clinic 注册的“转换函数”之一的名称。Clinic 将附带许多内置转换器;新转换器也可以动态添加。在选择转换器时,您会自动限制输入允许的 Python 类型,并指定输出变量(或变量)的类型。尽管许多转换器将类似于 C 类型或 Python 类型的名称,但转换器的名称可以是任何合法的 Python 标识符。

如果转换器后面跟着括号,这些括号包含转换函数的参数。语法模仿提供 Python 函数调用参数:参数必须始终命名,就像它们是“仅限关键字参数”一样,并且为参数提供的值在语法上将类似于 Python 字面值。这些参数始终是可选的,允许所有转换函数在不带任何参数的情况下被调用。在这种情况下,您也可以完全省略括号;这总是等同于指定空括号。为这些参数提供的值必须与 ast.literal_eval 兼容。

“default”是一个 Python 字面值。默认值是可选的;如果未指定,您也必须省略等号。没有默认值的参数是隐式必需的。默认值是动态分配的,在生成的 C 代码中是“活的”,尽管它被指定为 Python 值,但在生成的 C 代码中它被转换为原生 C 值。由于这种手动转换步骤,允许的默认值很少。

如果这是一个 Python 函数声明,参数声明将由尾随逗号或结束括号分隔。然而,Argument Clinic 既不使用;参数声明由换行符分隔。不允许尾随逗号或右括号。

第一个参数声明为特定 Clinic 代码块中的所有参数声明建立缩进。所有后续参数必须缩进到相同的级别。

旧式转换器

为了方便将现有代码转换为 Argument Clinic,Clinic 提供了一组与 PyArg_ParseTuple 格式单元匹配的旧式转换器。它们被指定为包含格式单元的 C 字符串。例如,要将参数“foo”指定为接受 Python“int”并发出 C int,您可以指定

foo : "i"

(为了更接近 C 字符串,这些必须始终使用双引号。)

尽管这些类似于 PyArg_ParseTuple 格式单元,但不能保证实现将调用 PyArg_Parse 函数进行解析。

此语法不支持参数。因此,它不支持任何需要输入参数的格式单元("O!", "O&", "es", "es#", "et", "et#")。需要这些转换之一的参数不能使用旧式语法。(但是,您仍然可以提供默认值。)

参数文档字符串

所有出现在参数声明下方并进一步缩进的行都是该参数的文档字符串。所有这些行都“去缩进”,直到第一行左对齐。

参数行的特殊语法

参数部分可以使用四个特殊符号。每个符号都必须单独占一行,缩进级别与参数声明相同。这四个符号是

*
确定所有后续参数都是仅限关键字的。
[
建立可选参数“组”的开始。请注意,“组”可以嵌套在其他“组”中。请参阅下面的仅限位置参数的函数。请注意,目前 [ 仅在所有参数都被标记为仅限位置的函数中合法使用,请参阅下面的 /
]
结束可选参数“组”。
/
确定所有前面的参数都是仅限位置的。目前,Argument Clinic 不支持同时具有仅限位置和非仅限位置参数的函数。因此:如果为函数指定了 /,则目前它必须始终位于最后一个参数之后。此外,Argument Clinic 目前不支持仅限位置参数的默认值。

/ 的语义遵循 Guido 曾经提出的 Python 中仅限位置参数的语法。[5]

函数文档字符串

函数声明后没有前导空格的第一行是函数文档字符串的第一行。Clinic 块的所有后续行都被视为文档字符串的一部分,并且其前导空格被保留。

如果字符串 {parameters} 在函数文档字符串中单独占一行,Argument Clinic 将插入一个包含所有带有文档字符串的参数的列表,每个这样的参数后跟其文档字符串。参数名称单独占一行;文档字符串从后续行开始,文档字符串的所有行都缩进两个空格。(没有每个参数文档字符串的参数将被抑制。)整个列表将缩进 {parameters} 标记之前出现的领先空白。

如果文档字符串中没有出现字符串 {parameters},Argument Clinic 将在文档字符串末尾附加一个,如果文档字符串没有以空行结尾,则在它上方插入一个空行,并且参数列表位于第 0 列。

转换器

Argument Clinic 包含预初始化的转换器函数注册表。示例转换器函数

int
接受实现 __int__ 的 Python 对象;发出 C int
byte
接受一个 Python int;发出一个 unsigned char。整数必须在 [0, 256) 范围内。
str
接受一个 Python str 对象;发出一个 C char *。使用 ascii 编码器自动编码字符串。
PyObject
接受任何对象;不进行任何转换,发出一个 C PyObject *

所有转换器都接受以下参数

doc_default
在 Python 上下文中,用于替代参数实际默认值的 Python 值。换句话说:如果指定,此值将用于文档字符串和 Signature 中参数的默认值。(待定替代语义:如果字符串是一个有效的 Python 表达式,可以使用 eval() 渲染为 Python 值,则 eval() 的结果将用作 Signature 中的默认值。)如果没有默认值,则忽略。
required
通常,任何具有默认值的参数都自动是可选的。将“required”设置为真的参数即使具有默认值也将被视为必需(非可选)。生成的文档也不会显示任何默认值。

此外,转换器可以单独接受一个或多个以下可选参数

annotation
明确指定此参数的每个参数注释。通常,生成注释(如果有)是转换函数的责任。
bitwise
对于接受无符号整数的转换器。如果传入的 Python 整数是有符号的,即使它是负数,也直接复制位。
encoding
对于接受 str 的转换器。将 Unicode 字符串编码为 char * 时使用的编码。
immutable
只接受不可变的值。
length
对于接受可迭代类型的转换器。请求转换器也发出可迭代对象的长度,该长度以 Py_ssize_t 变量的形式传递给 _impl 函数;其名称将是此参数的名称后附加“_length”。
nullable
此转换器通常不接受 None,但在这种情况下应该接受。如果在 Python 端提供了 None,则等效的 C 参数将是 NULL。(此转换器发出的 _impl 参数大概是指针类型。)
types
表示此对象可接受的 Python 类型的字符串列表。还有四个字符串代表 Python 协议
  • “buffer”
  • “mapping”
  • “number”
  • “sequence”
zeroes
对于接受字符串类型的转换器。转换后的值应允许包含嵌入的零。

返回转换器

返回转换器在概念上执行转换器的逆操作:它将原生 C 值转换为等效的 Python 值。

指令

Argument Clinic 还允许在 Clinic 代码块中使用“指令”。指令类似于 C 中的 pragmas;它们是修改 Argument Clinic 行为的语句。

指令的格式如下

directive_name [argument [second_argument [ ... ]]]

指令只接受位置参数。

Clinic 代码块必须包含一个或多个指令,或一个函数声明。它可以同时包含两者,在这种情况下,所有指令必须位于函数声明之前。

内部指令直接映射到 Python 可调用对象。指令的参数作为 str() 类型的位置参数直接传递给可调用对象。

可能的指令示例包括 Clinic 输出的生成、抑制或重定向。此外,“module”和“class”关键字在原型中作为指令实现。

Python 代码

Argument Clinic 还允许在 C 文件中嵌入 Python 代码,当 Argument Clinic 处理文件时,该代码会就地执行。嵌入代码如下所示

/*[python]

# this is python code!
print("/" + "* Hello world! *" + "/")

[python]*/
/* Hello world! */
/*[python end:da39a3ee5e6b4b0d3255bfef95601890afd80709]*/

上面 "/* Hello world! */" 行是通过运行前一个注释中的 Python 代码生成的。

任何 Python 代码都是有效的。Argument Clinic 中的 Python 代码部分也可以用于直接与 Clinic 交互;请参阅Argument Clinic 编程接口

输出

Argument Clinic 将其输出写入 C 文件中,紧跟在 Clinic 代码部分之后。对于“python”部分,输出是使用 builtins.print 打印的所有内容。对于“clinic”部分,输出是有效的 C 代码,包括

  • 一个 #define,提供函数的正确 methoddef 结构
  • “impl”函数的原型——这就是您将编写来实现在此函数的内容
  • 一个处理所有参数解析的函数,它调用您的“impl”函数
  • “impl”函数的定义行
  • 以及指示输出结束的注释。

其意图是您在输出之后立即编写 impl 函数的主体——也就是说,您在输出结束注释之后立即编写左大括号并在其中实现内置函数。(一开始有点奇怪,但却出奇地方便。)

Argument Clinic 将为您定义 impl 函数的参数。该函数将接受最初传入的“self”参数,您定义的所有参数,以及可能的一些额外生成的参数(“长度”参数;以及“组”参数,参见下一节)。

Argument Clinic 还会为输出部分写入校验和。这是一项有价值的安全功能:如果您手动修改输出,Clinic 会注意到校验和不匹配,并拒绝覆盖文件。(您可以使用命令行参数“-f”强制 Clinic 覆盖;当使用命令行参数“-o”时,Clinic 也会忽略校验和。)

最后,Argument Clinic 还可以发出已定义类和模块的 PyMethodDef 数组的样板定义。

仅限位置参数的函数

C 语言中实现的大部分 Python 内置函数都使用较旧的仅限位置 API 来处理参数 (PyArg_ParseTuple())。在某些情况下,这些内置函数根据传入的参数数量以不同的方式解析其参数。这可以提供一些令人困惑的灵活性:可能存在可选参数组,这些参数必须全部指定或全部不指定。而且偶尔这些组位于左侧!(一个有代表性的例子:curses.window.addch()。)

Argument Clinic 通过允许您按组指定参数来支持这些旧用例。每个可选参数组都用方括号标记。请注意,这些组允许在任何必需参数的右侧或左侧

Clinic 生成的 impl 函数将为每个组添加一个额外的参数,“int group_{left|right}_<x>”,其中 x 是一个单调递增的数字,在每个组从所需参数构建时分配给它。如果此调用指定了该组,则此参数将为非零,否则为零。

请注意,在此模式下操作时,您不能指定默认参数。

此外,请注意,可以为函数指定一组组,使得从参数数量到一组有效组存在多个有效映射。如果发生这种情况,Clinic 将中止并显示错误消息。这应该不是问题,因为仅限位置操作仅用于旧版用例,并且所有使用这种古怪行为的旧版函数都具有明确的映射。

当前状态

截至本文撰写时,Argument Clinic 的工作原型实现已在线提供(尽管您阅读本文时语法可能已过时)。[6] 该原型使用现有的 PyArg_Parse API 生成代码。它支持转换为所有当前格式单元,除了神秘的 "w*"。使用 Argument Clinic 的示例函数行使了所有主要功能,包括仅限位置参数解析。

Argument Clinic 编程接口

该原型目前还提供了一种实验性扩展机制,允许动态添加对新类型的支持。请参阅原型中的 Modules/posixmodule.c 以获取其用法示例。

将来,Argument Clinic 有望足够自动化,以允许通过 Python 代码查询、修改或完全新建函数声明。它甚至可能允许动态添加您自己的自定义 DSL!

备注 / 待定

  • 为内置函数提供 inspect.Signature 元数据的 API 目前正在讨论中。Argument Clinic 将在原型可行时添加支持。
  • Alyssa Coghlan 建议我们 a) 每个函数最多只支持一个左可选组,并且 b) 在存在歧义的情况下,优先选择左组而不是右组。这将解决我们所有现有用例,包括 range()。
  • 理想情况下,我们希望 Argument Clinic 作为正常 Python 构建过程的一部分自动运行。但这提出了一个引导问题;如果您没有系统 Python 3,您需要一个 Python 3 可执行文件来构建 Python 3。我确信这是一个可解决的问题,但我不知道最佳解决方案可能是什么。(支持这一点还需要为 Windows 提供并行解决方案。)
  • 与此相关:inspect.Signature 无法表示参数块,例如 curses.window.addch 的左可选块 yx。我们将在多大程度上支持这种公认的异常参数范式?
  • 在 PyCon US 2013 语言峰会上,讨论了让 Argument Clinic 也为函数生成实际文档(ReST 格式,由 Sphinx 处理)。这方面的具体细节待定,但这将要求文档字符串以 ReST 格式编写,并要求 Python 附带 ReST -> ascii 转换器。在我们将 CPython 源代码树大规模转换为使用 Clinic 之前,最好对此做出决定。
  • Guido 建议将“函数文档字符串”手写在输出中间,类似这样
    /*[clinic]
      ... prototype and parameters (including parameter docstrings) go here
    [clinic]*/
    ... some output ...
    /*[clinic docstring start]*/
    ... hand-edited function docstring goes here   <-- you edit this by hand!
    /*[clinic docstring end]*/
    ... more output
    /*[clinic output end]*/
    

    我尝试过这种方式,但我不喜欢它——我认为它很笨拙。我更喜欢将所有您编写的内容放在一个地方,而不是在 DSL 输出中间有一个手写编辑的东西。

  • Argument Clinic 不支持自动元组解包(PyArg_ParseTuple() 的“(OOO)”样式格式字符串)。
  • Argument Clinic 消除了某些动态性/灵活性。使用 PyArg_ParseTuple(),理论上可以在运行时为“es”/“et”格式单元传入不同的编码。据我所知,CPython 本身没有这样做,但是外部用户可能会这样做。(小知识:regrtest 没有使用“es”的情况,而所有使用“et”的情况都在 socketmodule.c 中,除了 _ssl.c 中的一个。它们都是静态的,指定编码 "idna"。)

致谢

PEP 作者衷心感谢 Ned Batchelder 允许他无耻地剽窃了 Cog 的巧妙设计——“我最喜欢的工具,但我从未真正使用过”。还要感谢所有在 [bugtracker issue] 和 python-dev 上提供反馈的人。特别感谢 Alyssa (Nick) Coghlan 和 Guido van Rossum 在 PyCon US 2013 上就该主题进行了两个小时的激动人心的面对面深入探讨。

参考资料


来源: https://github.com/python/peps/blob/main/peps/pep-0436.rst

最后修改: 2025-02-01 08:59:27 GMT