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

Python 增强提案

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

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