为什么我们有一个__feature__?

历史

在PySide用户故事PYSIDE-1019中,我们测试了某些方法以使PySide更加符合Python风格。第一个想法是支持某种方式允许snake_case函数名称。

此功能在兼容性问题上相对较低,因为拥有相同功能但不同名称的函数并不理想,但这可能是一个低成本的解决方案。

当涉及到true_property时,情况就变得不同了。当我们支持将属性作为一等对象而不是getter和setter函数时,就会出现冲突,因为一个函数不能同时作为属性(没有大括号)和函数。

这一考虑引导我们产生了以下想法: 功能必须按模块可选。

为什么每个模块都可以选择功能?

假设你有一些预先存在的代码。也许你使用了一些下载的代码或者你生成了一个接口文件。当你现在决定使用一个功能时,你不希望所有这些现有的东西变得不正确。通过使用语句

from __feature__ import ...

你声明这个模块使用了一些特性。其他模块不会受到这个决定的影响,可以保持不变。

为什么是dunder,而不是__future__?

特别是在Python 2中,但在某些情况下也在Python 3中,有future语句

from __future__ import ...

这是一个只能在模块开头出现的语句,它会改变Python解析器的工作方式。

我们的第一个想法是为PySide模仿这种行为,尽管我们有点作弊:特性声明不是一个语法结构,我们不能轻易禁止它出现在模块的中间。

然后我们意识到Python的__future__导入和PySide的__feature__导入的意图是不同的:虽然Python通过__future__暗示了一些改进,但我们不想与__feature__相关联。我们只是认为一些来自Python的用户可能会喜欢我们的功能,而其他人则习惯于C++的惯例,并认为偏离Qt文档的内容是一个缺点。

使用from __feature__ import ...符号的意图是希望人们看到它与Python的__future__语句的相似性,并将该导入放在模块的开头,以便非常明显地表明该模块具有一些特殊的全局差异。

snake_case 功能

通过使用语句

from __feature__ import snake_case

此模块中使用的所有类的所有方法都在更改它们的名称。

更改名称的算法如下:

  • 如果名称少于3个字符,或者

  • 如果两个大写字符相邻,或者

  • 如果名称以 gl 开头(表示 OpenGL),

  • 名称保持不变。否则

  • 单个大写字母 C 被替换为 _c

true_property 特性

通过使用语句

from __feature__ import true_property

在此模块中使用的所有类的方法,如果在Qt文档中声明为属性,则在Python中成为真正的属性。

此功能与过去不兼容,无法共存;这就是为什么开发这个功能想法的原因。

常规属性

普通属性的名称与之前相同:

QtWidgets.QLabel().color()

成为属性

QtWidgets.QLabel().color

当还有一个setter方法时,

QtWidgets.QLabel().setColor(value)

成为属性

QtWidgets.QLabel().color = value

普通属性会吞没getter和setter函数,并用属性对象替换它们。

特殊属性

特殊属性是指那些具有非标准名称的属性。

QtWidgets.QLabel().size()

成为属性

QtWidgets.QLabel().size

但这里我们没有setSize函数,而是

QtWidgets.QLabel().resize(value)

这成为属性

QtWidgets.QLabel().size = value

在这种情况下,setter 不会被忽略,因为很多人已经习惯了 resize 函数。

类属性

应该提到的是,我们不仅支持Python中已知的常规属性。还有类属性的概念,它们总是调用它们的getter和setter:

像前面提到的QtWidgets.QLabel这样的常规属性具有以下可见性:

>>> QtWidgets.QLabel.size
<property object at 0x113a23540>
>>> QtWidgets.QLabel().size
PySide6.QtCore.QSize(640, 480)

类属性也不需要实例即可评估:

>>> QtWidgets.QApplication.windowIcon
<PySide6.QtGui.QIcon(null) at 0x113a211c0>

只有直接访问正确的类字典才能检查它:

>>> QtGui.QGuiApplication.__dict__["windowIcon"]
<PySide6.PyClassProperty object at 0x114fc5270>

关于属性完整性

有许多属性,Python程序员一致认为这些函数应该是属性,但有一些不是属性,比如

>>> QtWidgets.QMainWindow.centralWidget
<method 'centralWidget' of 'PySide6.QtWidgets.QMainWindow' objects>

我们目前正在讨论是否应该纠正这些罕见的情况,因为它们可能只是遗漏。记住缺失的属性似乎相当麻烦,与其在Qt文档中查找所有属性,不如添加所有应该是属性且明显缺失的属性会更容易。

名称冲突及解决方案

在某些罕见的情况下,属性已经作为一个函数存在,要么有多个签名,要么有参数。这在C++中也不是很好,但在Python中这是被禁止的。例如:

>>> from PySide6 import *
>>> from PySide6.support.signature import get_signature
>>> import pprint
>>> pprint.pprint(get_signature(QtCore.QTimer.singleShot))
[<Signature (arg__1: int, arg__2: Callable) -> None>,
 <Signature (msec: int, receiver: PySide6.QtCore.QObject, member: bytes) -> None>,
 <Signature (msec: int, timerType: PySide6.QtCore.Qt.TimerType,
                        receiver: PySide6.QtCore.QObject, member: bytes) -> None>]

在创建此属性时,我们尊重现有函数,并通过附加下划线为该属性使用一个稍有不同的名称。

>>> from __feature__ import true_property
>>> QtCore.QTimer.singleShot_
<property object at 0x118e5f8b0>

我们希望这些冲突能在未来的Qt版本中得到解决。

__feature__ 导入

from __feature__ import ... 的实现是通过对内置的 __import__ 进行轻微修改来构建的。我们通过在内置模块中分配变量来明确这一点。此修改在 Qt for Python 导入时发生:

  • 原始函数在 __import__ 中保留在 __orig_import__ 中。

  • 新功能在 __feature_import__ 中,并分配给 __import__

此函数首先调用Python函数PySide6.support.__feature__.feature_import,如果功能导入不适用,则回退到__orig_import__

覆盖 __import__

不建议这样做。导入修改应使用导入钩子完成,请参阅Python文档中的Import-Hooks

如果您希望在不破坏功能的情况下修改__import__,请仅覆盖__orig_import__函数。

集成开发环境和修改Python存根文件

Qt for Python 提供了预生成的 .pyi 存根文件,这些文件与二进制模块位于同一位置。例如,在 site-packages 目录中,你可以在 QtCore.abi3.so 或 Windows 上的 QtCore.pyd 旁边找到一个 QtCore.pyi 文件。

在使用__feature__时,通常与常见的IDE一起使用,您可能希望提供一个功能感知版本的.pyi文件以获得正确的显示。最简单的方法是使用以下命令一次性更改它们:

pyside6-genpyi all --feature snake_case true_property

使用 __feature__ 与 UIC 文件

功能可以与生成的UIC文件自由结合使用。UIC文件故意不被转换。将它们与其他Python模块中的功能选择混合使用应该总是有效的,因为切换将根据需要由当前活动的模块选择。(如果某个示例失败,请向我们报告)