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 已被 Guido van Rossum 在 2013 年美国 PyCon 上拒绝。但是,此 PEP 中提出的几个具体问题在设计 PEP 436 DSL 的第二次迭代 时已考虑在内。
基本原理
关于 PEP 436 DSL 在 C 文件上下文中的适用性存在不同意见。此 PEP 提出了一种替代的 DSL。在本文档的最后一节中将解释导致反提案的 PEP 436 的具体问题。
范围
PEP 专注于 DSL 本身。文档字符串的输出位置或生成的代码等主题不在本 PEP 的范围内。
但是,至关重要的是,DSL 适用于生成自定义参数解析器,这是一个已在 Cython 中实现的功能。因此,本 PEP 的目标之一是使 DSL 接近现有解决方案,从而促进将 Cython 的相关部分包含到 CPython 源代码树中。
DSL 概述
类型安全和注解
从 Python 值到 C 值的转换完全由转换器函数的类型定义。除了众所周知的默认转换器“i”、“f”等之外,PyArg_Parse* 函数族还接受自定义转换器。
此 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 的定义。各个部分将在详细中解释。从语法上讲,整个 define 块由函数规范和输出部分组成。函数规范又由声明部分、可选的 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 块
函数规范块以 /*[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.h 自动生成 converters.py 模块。
参考实现
参考实现可在 问题 16612 中获得。由于此 PEP 是在时间限制下编写的,并且作者不熟悉 PLY 工具链,因此该软件是用 Standard ML 编写的,并使用 ml-yacc/ml-lex 工具链。
语法是无冲突的,并且以 ml-yacc 可读的 BNF 形式提供。
有两个工具可用
- printsemant 读取转换器头文件和 .c 文件,并将语义检查的解析树转储到标准输出。
- preprocess 读取转换器头文件和 .c 文件,并将预处理的 .c 文件转储到标准输出。
已知缺陷
- Python 'test' 表达式未进行语义检查。但是,语法已检查,因为它属于语法的一部分。
- 词法分析器不处理三重引号字符串。
- C 声明以原始方式解析。最终实现应利用 C 语法中的 'declarator' 和 'init-declarator'。
- preprocess 工具不为左右可选参数情况发出代码。printsemant 工具可以处理这种情况。
- 由于 preprocess 工具从解析树生成输出,因此 define 块的原始缩进会丢失。
语法
待定:语法以 ml-yacc 可读的形式存在,但可能应在此处以 EBNF 表示法包含。
与 PEP 436 的比较
此 PEP 的作者对 PEP 436 中提出的 DSL 存在以下担忧
- 类似于配置文件的空格敏感语法在 C 文件中显得格格不入。
- 函数定义的结构在每个参数的规范中丢失了。诸如仅位置、必需和仅关键字之类的关键字分散在太多不同的位置。
相比之下,在替代 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
上次修改时间: 2023-09-09 17:39:29 GMT