为什么我们要有这样一个__功能__?#

历史#

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

由于具有不同名称的同一函数不会太美观,但这是一个可能的低成本低代价的解决方案,因此该功能相对较容易实现。

当我们支持将属性作为一等对象而不是获取函数和设置函数来支持时,事情变得不同。由于函数不能同时作为属性(没有花括号)和函数存在,我们得到一个冲突。

这种考虑使我们产生了这样的想法:功能必须按模块可选择性。

为什么功能是按模块选择的?#

假设你有一些现有的代码。可能你是使用某些下载的代码,或者你生成了一个接口文件。当你决定使用一个功能时,你不希望所有这些现有内容都变得不正确。通过使用以下语句

from __feature__ import ...

你声明该模块使用某些功能。其他模块不会受到此决定的影响,可以保持不变。

为什么使用双下划线和为什么不使用__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__ 语句的相似性,并将该导入放在模块开头,以使非常明显地表明此模块有一些特殊的全局差异。

蛇形命名特性#

使用以下语句

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

如果有设置器方法时,

QtWidgets.QLabel().setColor(value)

变为属性

QtWidgets.QLabel().color = value

常规属性会吞掉获取器和设置器函数,并用属性对象替换它们。

特殊属性#

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

QtWidgets.QLabel().size()

变为属性

QtWidgets.QLabel().size

但这里没有 setSize 函数,而是

QtWidgets.QLabel().resize(value)

变成属性

QtWidgets.QLabel().size = value

在这种情况下,设置器没有被吞掉,因为很多人习惯于使用 resize 函数。

类属性#

需要注意的是,我们不仅支持常规属性。还有类属性的概念,总是调用它们的获取器和设置器

上述常规属性 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__#

这并不建议使用。应使用 import hooks 来进行导入修改,请参阅 Python 文档中的 Import-Hooks

如果您仍然想修改 __import__ 而不破坏功能,请仅重写 __orig_import__ 函数。

IDEs 和修改 Python 模块文件#

Qt for Python 随附预生成的 .pyi 模块文件,与二进制模块位于同一位置。例如,在 site-packages 目录中,您可以在 QtCore.pyi 文件旁边找到 QtCore.abi3.so 或 Windows 上的 QtCore.pyd

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

pyside6-genpyi all --feature snake_case true_property

使用 __feature__ 与 UIC 文件#

功能可以自由地与生成的 UIC 文件一起使用。有意将这些文件转换为 UIC 文件。将它们与其他 Python 模块中的功能选择混合应始终正常工作,因为切换将在需要时发生,由当前活动模块选择。(如果对示例失败,请向我们报告)