Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 增强提案

PEP 436 – 参数诊所 DSL

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

目录

摘要

本文档提出“参数诊所”,这是一种 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 非常复杂,以至于使用起来有些痛苦。考虑一下

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

参数诊所的目标是用一种不继承任何这些缺点的机制来替换此 API

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

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

参数诊所的未来目标包括

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

DSL 语法摘要

参数诊所 DSL 被指定为嵌入在 C 文件中的注释,如下所示。右侧的“示例”列向您展示了参数诊所 DSL 的示例输入,左侧的“部分”列依次指定了每一行代表的内容。

参数诊所的 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 语法有所了解,以下是一些诊所代码块示例。第一个代码块反映了通常首选的样式,包括参数和每个参数的文档字符串之间的空行。它还包括一个在本地创建的用户定义转换器(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]
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]
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]*/

参数诊所 DSL 的通用行为

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

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

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

模块和类声明

当 C 文件实现模块或类时,应将其声明给诊所。语法很简单

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”语法是可选的。参数诊所使用函数的名称来创建生成的 C 函数的名称。在某些情况下,生成的名称可能会与 C 程序命名空间中的其他全局名称冲突。“as legal_c_id”语法允许您用自己的名称覆盖生成的名称;用任何合法的 C 标识符替换“legal_c_id”。如果跳过,则也必须省略“as”关键字。

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

参数声明

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

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

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

“converter”是向参数诊所注册的“转换器函数”之一的名称。诊所将附带许多内置转换器;还可以动态添加新的转换器。在选择转换器时,您会自动限制输入上允许的 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 整数;发出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 中的pragma;它们是修改 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 代码,包括

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

我们的意图是让你在输出之后立即编写 impl 函数的主体——也就是说,在输出结束注释之后立即写一个左花括号,并在那里实现内建函数。(一开始有点奇怪,但奇怪地很方便。)

Argument Clinic 会为你定义 impl 函数的参数。该函数将接收最初传入的“self”参数、你定义的所有参数,以及一些可能生成的额外参数(“length”参数;还有“group”参数,参见下一节)。

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

最后,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 的示例函数练习了所有主要功能,包括仅限位置的参数解析。

参数诊所编程接口

该原型目前还提供了一种实验性的扩展机制,允许动态添加对新类型的支持。请参阅原型中的 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.addchyx 的左可选块。在支持这种公认的异常参数范式方面,我们要走多远?
  • 在 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 问题] 和 python-dev 上提供反馈的人。特别感谢 Alyssa (Nick) Coghlan 和 Guido van Rossum 在 PyCon US 2013 上就该主题进行了为时两小时的现场深入探讨。

参考文献


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

上次修改时间:2023-10-11 12:05:51 GMT