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 已被 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