签名C扩展¶
此模块是CPython 3.5及以上版本以及CPython 2.7的C扩展。
其目的是为内置PyCFunction对象的__signature__
属性提供支持。
主题简介¶
从CPython 3.5开始,Python函数开始为普通的Python函数增加一个__signature__
属性。这完全是可选的,只是Python中的一个不错的功能。
另一方面,PySide 非常需要 __signature__
,因为 15000 多个 PySide 函数的类型信息确实缺失,如果能直接获取这些信息将会非常有用。
支持签名的想法¶
我们希望在所有PySide方法中增加一个__signature__
属性,而不需要改变大量生成的代码。
因此,我们没有改变任何现有的数据结构,
而是通过一个全局字典来支持这个新属性。
当请求__signature__
属性时,会调用一个方法在全局字典中进行查找。这是一种灵活的方法,对项目的其余部分影响很小。与直接属性访问相比,它的开销非常有限,但对于偶尔需要访问签名的需求来说,这是一个适当的折衷方案。
这段代码如何工作¶
仅支持常规Python函数的签名。为PyCFunction
对象创建签名在Python中需要相当多的额外工作。
幸运的是,我们发现了这种特殊的隐身技术,它为我们节省了大部分所需的努力:
基本思想是创建一个具有varnames、defaults和annotations属性的虚拟Python函数,然后使用inspect模块创建一个签名对象。该对象作为真实PyCFunction
对象的__signature__
属性的计算结果返回。
有一件事真的让Python有些改变:
我们为每个函数添加了
__signature__
属性。
这是对Python的一个小改动,虽然无害,但它为我们节省了大量的代码,这些代码在模块的早期版本中是必需的。
内部工作分为两个步骤:
当模块被导入时,类的所有函数都会获得签名文本。 这只是启动时间中增加的一个非常小的开销。对于整个类来说,它只是一个字符串。
实际的签名对象是在稍后真正请求属性时创建的。签名会被缓存,并且只在第一次访问时创建。
示例:
PyCFunction
QtWidgets.QApplication.palette
被询问其签名。这意味着调用了 pyside_sm_get___signature__()
。它调用 GetSignature_Function
,如果找到签名,则返回签名。
为什么这段代码很快¶
遍历每个签名对象需要花费一点时间(大约6秒),因为这些对象超过25000个Python对象。但所有签名对象很少被访问,除非在特殊应用中。通常情况下只有少数访问,这些访问速度非常快。
使这个签名模块快速的关键是尽可能避免计算。当没有使用签名对象时,初始化几乎不会浪费时间。只有在import PySide6
时,才会额外加载上述提到的字符串和一些支持模块。当涉及到签名使用时,会使用延迟初始化并进行缓存。这种技术在haskell中也被称为完全惰性。
实际上有两个地方会发生延迟初始化:
dict
可以不是字典而是元组。这是由PySide_BuildSignatureArgs
在模块加载时保存的初始参数元组。 如果是这样,那么将调用 parser.py 中的pyside_type_init
,它会解析字符串并创建字典。props
可以为空。然后调用 loader.py 中的create_signature
,它使用一个虚拟函数通过 inspect 模块生成一个签名实例。
始终执行的初始化只是每个类的两次字典写入,我们有大约1000个类。
为了测量额外的开销,我们模拟了当执行from PySide6 import *
时会发生什么。
结果表明,开销低于0.5毫秒。
签名包结构¶
与签名模块相关的C++代码完全位于文件
shiboken6/libshiboken/signature.cpp 中。所有其他功能都在
signature
Python包中实现。它具有以下结构:
sources/shiboken6/shibokenmodule/files.dir/shibokensupport
├── __init__.py
├── feature.py
├── fix-complaints.py
├── shibokensupport.pyproject
└── signature
├── PSF-3.7.0.txt
├── __init__.py
├── errorhandler.py
├── importhandler.py
├── layout.py
├── lib
│ ├── __init__.py
│ ├── enum_sig.py
│ ├── pyi_generator.py
│ └── tool.py
├── loader.py
├── mapping.py
├── parser.py
└── qt_attribution.json
真正重要的是parser、mapping、errorhandler、enum_sig、layout和loader模块。其余部分是为了创建Python 2兼容性或与嵌入和安装程序兼容而需要的。
- loader.py
该模块组装并导入
inspect
模块,然后导出create_signature
函数。该函数接受一个假函数和一些属性,并使用inspect模块构建一个__signature__
对象。- parser.py
该模块从C++中获取类签名字符串,并将其解析为
create_signature
函数所需的属性。其入口点是pyside_type_init
函数,该函数通过loader.py
从C模块中调用。- mapping.py
映射模块的目的是维护一个替换字符串列表,这些字符串将C中的签名文本映射到Python所需的属性字符串。许多映射通过
parser.py
中的相当复杂的表达式来解析,但在这里明确拼写几百个案例会更好。- errorhandler.py
自从
Qt For Python 5.12
版本以来,我们不再使用 C++ 的内置类型错误消息。 相反,我们使用签名模块获得了更好的结果。同时, 这也强制支持了 shiboken,并且签名模块不再是可选的。- enum_sig.py
签名模块的各种应用都需要遍历模块、类和函数。为了集中这种枚举,该过程已被提取为一个上下文管理器。用户只需提供实际进行格式化的函数。
例如,参见 .pyi 生成器
pyside6/PySide6/support/generate_pyi.py
。- layout.py
随着越来越多的应用程序使用签名模块,需要不同的签名格式。为了支持这一点,我们创建了函数
create_signature
,它有一个参数可以选择一些预定义的布局。- typing27.py
Python 2 完全没有 typing 模块。这是所需最小功能的后移植版本。
- backport_inspect.py
Python 2 有一个 inspect 模块,但完全缺少签名功能。 这个模块添加了缺失的功能,这些功能在运行时被合并到 inspect 模块中。
多重参数¶
到目前为止被忽略的一个方面是多重参数:当一个函数有多个签名时如何处理?
我没有找到关于在Python中如何处理多个签名的任何说明, 但这些简单的规则似乎很有效:
如果有一个列表,那么它是一个多重签名。
否则,它是一个简单的签名。
签名模块的影响¶
签名模块对其他PySide模块有许多影响,这些影响是由于它的存在而产生的,未来还会有更多影响:
existence_test.py¶
文件 pyside6/tests/registry/existence_test.py
是使用签名模块中的签名编写的。其想法是有大约15000个具有特定签名的函数。
这些函数不应因某些不良的签入而丢失。因此,所有现有签名的列表被保留为一个模块,该模块组装了一个字典。检查函数的存在性,以及确切的参数数量。
该模块存在于每个PySide版本和每个平台中。初始模块生成一次并保存为exists_{plat}_{version}.py
。
错误通常只作为警告报告,但是:
与Coin模块的交互¶
当这个测试程序在COIN中运行时,警告会被转换为错误。原因是只有在COIN中,我们有一个稳定的PySide模块配置,可以可靠地进行比较。
这些模块的名称为exists_{platf}_{version}_ci.py
,作为生成代码的一个大例外,这些文件是有意被检入的。
当列表缺失时会发生什么?¶
当创建新版本的PySide时,初始情况下不存在存在性测试文件。
当运行COIN测试时,它会报告错误并在标准输出上创建缺失的模块。 但由于COIN测试会多次运行,第一次测试生成的输出在后续运行中仍然存在。 (如果COIN被正确实现,我们就无法利用这一点,并且需要将其作为额外的异常来实现。)
因此,缺失的模块将被报告为部分成功的测试(称为“FLAKY”)。为了避免进一步的不可靠测试并激活为真正的测试,我们现在可以捕获COIN的错误输出并检查生成的模块。
显式强制重新创建¶
以前重新生成注册表文件的方法是删除文件并检查。这达到了预期的效果,但会产生巨大的差异。作为一种更有效的方法,我们在第一行准备了一个包含“recreate”一词的注释。通过取消注释这一行,会触发一个NameError,从而达到相同的效果。
init_platform.py¶
为了生成exists_{platf}_{version}
模块,编写了模块
pyside6/tests/registry/init_platform.py
。它可以从命令行独立使用,直接检查某些更改的兼容性。
scrape_testresults.py¶
为了简化和自动化提取exists_{platf}_{version}_ci.py
文件的过程,编写了脚本pyside6/tests/registry/scrape_testresults.py
。
此脚本扫描整个 PySide 的测试结果网站,即:
https://testresults.qt.io/coin/api/results/pyside/pyside-setup/
在第一次扫描时,脚本运行时间不到30分钟。之后,会生成一个缓存,扫描速度会快得多。测试结果会被放入文件夹pyside6/tests/registry/testresults/embedded/
中,并带有唯一的名称,便于排序。例如:
testresults/embedded/2018_09_10_10_40_34-test_1536891759-exists_linux_5_11_2_ci.py
这些文件只创建一次。如果它们已经存在,则不会再次被修改。
文件pyside6/tests/registry/known_urls.json`保存了成功扫描后所有扫描到的URL。testresults/embedded
文件夹可以保留以供参考或可以删除。重要的是只有json文件。
扫描的结果会直接放入pyside6/tests/registry/
文件夹中。应该进行审查,然后最终提交。
generate_pyi.py¶
pyside6/PySide6/support/generate_pyi.py
仍在开发中。
该模块生成所谓的提示存根,用于将 PySide 与各种 Python IDE 集成。
尽管此模块将存根创建为附加组件,但对签名模块质量的影响是相当大的:
模块必须创建语法正确的.pyi
文件,这些文件不仅包含签名,还包含所有PySide模块的常量和枚举。这作为一个额外的挑战,对签名的完整性和正确性有非常积极的影响。
该模块有一个--feature
选项来生成修改后的.pyi文件。
此命令的快捷方式是pyside6-genpyi
。
一个有用的命令是将所有 .pyi 文件更改为使用所有功能
pyside6-genpyi all --feature snake_case true_property
pyi_generator.py¶
shiboken6/shibokenmodule/files.dir/shibokensupport/signature/lib/pyi_generator.py
已从generate_pyi.py
中提取出来。它允许从使用shiboken创建的任意扩展模块生成.pyi
文件。
此命令的快捷方式是 shiboken6-genpyi
。
当前扩展¶
在签名模块编写之前,已经存在签名的概念,但更偏向于C++的方式。从那时起,就存在错误信息,这些信息是在函数接收到错误的参数类型时生成的。
这些错误消息被签名模块按需生成的文本所取代,以使其更加一致和正确。
这是在Qt For Python 5.12.0
中实现的。
此外,PySide方法的__doc__
属性未设置。
通过创建签名作为文档字符串的默认内容,很容易获得一个很好的help()
功能。
这是在Qt For Python 5.12.1
中实现的。
签名模块的更新与未来¶
PYSIDE-2101: The __signature__ attribute is gone due to rlcompleter.
2022年底,对rlcompleter模块的更改使得无法继续支持PySide中的非官方__signature__
属性。从那时起,签名的功能由get_signature
函数保留。
多年来,对生成的pyi文件正确性的要求急剧增加,并且投入了大量精力来确保生成的.pyi
文件对于当前的mypy
工具是正确的。关于已纠正错误类型的更多信息可以在mypy-correctnes部分找到。
文献¶
个人备注:这个模块献给我们的小鸟“Püppi”,它于2017年9月15日去世。