PEP 437 – 用于指定签名、注解和参数转换器的 DSL
- 作者:
- Stefan Krah <skrah at bytereef.org>
- 状态:
- 已拒绝
- 类型:
- 标准跟踪
- 创建日期:
- 2013 年 3 月 11 日
- Python 版本:
- 3.4
- 发布历史:
- 决议:
- Python-Dev 消息
摘要
Python C-API 目前没有用于指定和自动生成函数签名、注解或自定义参数转换器的机制。
解决这个问题有几种可能的方法。Cython 在 .pyx 文件中使用 cdef 定义来生成所需信息。然而,CPython 的 C-API 函数通常需要额外的初始化和清理代码片段,这些代码片段很难在 cdef 中指定。
PEP 436 提出了一个嵌入在 C 注释中的领域特定语言 (DSL),它在很大程度上类似于一个按参数配置的文件。预处理器读取注释并发出一个参数解析函数、文档字符串和利用解析结果的函数的头文件。
后者函数随后被称为 实现函数。
拒绝通知
此 PEP 在 2013 年 PyCon US 上被 Guido van Rossum 否决。然而,在设计 PEP 436 DSL 的第二次迭代时,此 PEP 提出的一些具体问题被考虑在内。
基本原理
关于 PEP 436 DSL 在 C 文件上下文中的适用性存在不同意见。此 PEP 提出了另一种 DSL。此 PEP 的最后一节将解释促使提出反建议的 PEP 436 的具体问题。
范围
此 PEP 仅关注 DSL。文档字符串或生成代码的输出位置等主题超出了此 PEP 的范围。
然而,DSL 必须适合生成自定义参数解析器,这是 Cython 中已经实现的功能。因此,此 PEP 的目标之一是使 DSL 接近现有解决方案,从而促进将 Cython 的相关部分可能包含到 CPython 源代码树中。
DSL 概述
类型安全和注解
从 Python 值到 C 值的转换由转换器函数的类型完全定义。PyArg_Parse* 函数家族除了众所周知的默认转换器“i”、“f”等之外,还接受自定义转换器。
此 PEP 将默认转换器视为抽象函数,无论它们实际如何实现。
Include/converters.h
转换器函数必须提前声明。所有转换器函数都应输入到文件 Include/converters.h 中。该文件在翻译 .c 文件之前由预处理器读取。这是一个摘录
/*[converter]
##### Default converters #####
"s": str -> const char *res;
"s*": [str, bytes, bytearray, rw_buffer] -> Py_buffer &res;
[...]
"es#": str -> (const char *res_encoding, char **res, Py_ssize_t *res_length);
[...]
##### Custom converters #####
path_converter: [str, bytes, int] -> path_t &res;
OS_STAT_DIR_FD_CONVERTER: [int, None] -> int res;
[converter_end]*/
转换器由其名称、Python 输入类型和 C 输出类型指定。默认转换器必须有带引号的名称,自定义转换器必须有常规名称。Python 类型由其名称给出。如果一个函数接受多个 Python 类型,则该集合以列表形式书写。
由于默认转换器可能具有多个隐式返回值,因此 C 输出类型根据以下约定书写
主返回值必须命名为 res。这是稍后在 DSL 中给出的实际变量名称的占位符。额外的隐式返回值必须以 res_ 为前缀。
默认情况下,变量通过值传递给实现函数。如果应传递地址,则 res 必须以 & 符号为前缀。
附加声明可以放入 .c 文件中。只要函数类型相同,允许重复声明。
鼓励在转换器函数定义上方第二次声明自定义转换器类型。预处理器将捕获声明之间的任何不匹配。
为了保持转换器复杂性可控,PY_SSIZE_T_CLEAN 将被弃用,并且所有长度参数都将假定为 Py_ssize_t。
待办:列出幻想类型,如 rw_buffer。
函数规范
关键字参数
此示例包含 os.stat 的定义。各个部分将详细解释。在语法上,整个定义块由一个函数规范和一个输出部分组成。函数规范反过来由一个声明部分、一个可选的 C-声明部分和一个可选的清理代码部分组成。函数规范中的部分在 yacc 样式中通过“%%”分隔
/*[define posix_stat]
def os.stat(path: path_converter, *, dir_fd: OS_STAT_DIR_FD_CONVERTER = None,
follow_symlinks: "p" = True) -> os.stat_result: pass
%%
path_t path = PATH_T_INITIALIZE("stat", 0, 1);
int dir_fd = DEFAULT_DIR_FD;
int follow_symlinks = 1;
%%
path_cleanup(&path);
[define_end]*/
<literal C output>
/*[define_output_end]*/
定义块
函数规范块以 /*[define 标记开头,后跟一个可选的 C 函数名称,然后是一个右括号。如果未给出 C 函数名称,则从声明名称生成。在示例中,省略名称 posix_stat 将导致 C 函数名称为 os_stat。
声明
所需的声明(几乎)是一个有效的 Python 函数定义。“def”关键字和函数体是冗余的,但此 PEP 的作者认为如果它们存在,定义更具可读性。
函数名称可以是一个路径而不是一个普通标识符。每个参数都使用将应用于它的转换器函数的名称进行注解。
默认值以通常的 Python 方式给出,可以是任何有效的 Python 表达式。
返回值可以是任何 Python 表达式。通常它将是一个对象的名称,但替代返回值可以以列表形式指定。
C-声明
此可选部分包含 C 变量声明。由于转换器函数已提前声明,预处理器可以对声明进行类型检查。
清理
可选的清理部分包含字面 C 代码,这些代码将在实现函数之后未修改地插入。
输出
输出部分包含预处理器发出的代码。
仅限位置参数
不接受关键字参数的函数通过存在 斜杠 特殊参数来指示
/*[define stat_float_times]
def os.stat_float_times(/, newval: "i") -> os.stat_result: pass
%%
int newval = -1;
[define_end]*/
预处理器将此定义转换为 PyArg_ParseTuple() 调用。斜杠右侧的所有参数都是可选参数。
左右可选参数
一些旧版函数在中心参数的左右两侧都包含可选参数组。一个新工具是否应该支持此类函数是值得商榷的。为了完整起见,这是建议的语法
/*[define]
def curses.window.addch(y: "i", x: "i", ch: "O", attr: "l") -> None: pass
where groups = [[ch], [ch, attr], [y, x, ch], [y, x, ch, attr]]
[define_end]*/
这里 ch 是中心参数,attr 可以在右侧可选添加,并且组 [y, x] 可以在左侧可选添加。
本质上,规则是中心参数和可选组的所有有序组合都必须是可能的,以便没有两个组合具有相同的长度。
这可以通过将中心参数首先放在列表中,然后将可选参数组添加到左右两侧来简洁地表达。
格式灵活性
如果上面的 os.stat 示例被认为过于紧凑,可以很容易地按如下方式格式化
/*[define posix_stat]
def os.stat(path: path_converter,
*,
dir_fd: OS_STAT_DIR_FD_CONVERTER = None,
follow_symlinks: "p" = True)
-> os.stat_result: pass
%%
path_t path = PATH_T_INITIALIZE("stat", 0, 1);
int dir_fd = DEFAULT_DIR_FD;
int follow_symlinks = 1;
%%
path_cleanup(&path);
[define_end]*/
<literal C output>
/*[define_output_end]*/
紧凑记法的优点
当涉及大量参数时,简洁记法的优点尤其明显。_posixsubprocess.fork_exec 的参数解析部分由以下定义完全指定
/*[define subprocess_fork_exec]
def _posixsubprocess.fork_exec(
process_args: "O", executable_list: "O",
close_fds: "p", py_fds_to_keep: "O",
cwd_obj: "O", env_list: "O",
p2cread: "i", p2cwrite: "i", c2pread: "i", c2pwrite: "i",
errread: "i", errwrite: "i", errpipe_read: "i", errpipe_write: "i",
restore_signals: "i", call_setsid: "i", preexec_fn: "i", /) -> int: pass
[define_end]*/
请注意,preprocess 工具目前为此示例发出了一个冗余的 C 声明部分,因此输出比必要长度要长。
易于验证定义
一个经验不足的用户如何验证像 os.stat 这样的定义?只需将 os.stat 更改为 os_stat,定义缺少的转换器并将定义粘贴到 Python 交互式解释器中!
事实上,一个 converters.py 模块可以从 converters.h 自动生成。
参考实现
参考实现可在 issue 16612 中找到。由于此 PEP 是在时间限制下编写的,并且作者不熟悉 PLY 工具链,因此该软件是用 Standard ML 编写的,并利用 ml-yacc/ml-lex 工具链。
语法无冲突,并以 ml-yacc 可读的 BNF 形式提供。
提供了两个工具
- printsemant 读取转换器头文件和 .c 文件,并将经过语义检查的解析树转储到 stdout。
- preprocess 读取转换器头文件和 .c 文件,并将预处理过的 .c 文件转储到 stdout。
已知缺陷
- Python 的“test”表达式未进行语义检查。然而,由于它是语法的一部分,因此语法经过检查。
- 词法分析器不处理三引号字符串。
- C 声明以原始方式解析。最终实现应利用 C 语法中的“declarator”和“init-declarator”。
- preprocess 工具不为左右可选参数情况发出代码。printsemant 工具可以处理这种情况。
- 由于 preprocess 工具从解析树生成输出,因此定义块的原始缩进会丢失。
语法
待办:语法以 ml-yacc 可读形式存在,但可能应该以 EBNF 符号包含在此处。
与 PEP 436 的比较
此 PEP 的作者对 PEP 436 中提出的 DSL 有以下担忧
- C 文件中对空白敏感的类似配置文件的语法显得格格不入。
- 函数定义的结构在每个参数的规范中丢失了。像 positional-only、required 和 keyword-only 这样的关键字散布在太多不同的地方。
相比之下,在替代 DSL 中,函数定义的结构一目了然。
- PEP 436 DSL 有 14 个有文档说明的标志和至少一个无文档说明的(allow_fd)标志。弄清楚 2**15 种可能组合中的哪些是有效的,给用户带来了不必要的负担。
使用 PEP 3118 缓冲区标志的经验表明,整理(并穷尽测试!)有效组合是一项极其繁琐的任务。PEP 3118 标志仍然不被许多人很好地理解。
相比之下,替代 DSL 有一个中央文件 Include/converters.h,可以快速搜索所需的转换器。许多转换器已经为人所知,甚至可能被人们记住(由于频繁使用)。
- PEP 436 DSL 允许过多的自由。类型显然可以省略,预处理器接受(并忽略)未知关键字,有时在文档字符串后添加空白会导致断言错误。
另一方面,替代 DSL 不允许这种自由。省略转换器或返回值注解显然是语法错误。LALR(1) 语法是明确的,并且为完整的翻译单元指定。
版权
本文档根据 开放出版许可证 获得许可。
来源:https://github.com/python/peps/blob/main/peps/pep-0437.rst