为什么 PyQt 类 的反弹方法会引发 TypeError

Why do rebound methods of PyQt classes raise a TypeError

使我的代码与 PyQt5/6 和 PySide2/6 兼容,我写了

if not hasattr(QtCore.QDateTime, 'toPython'):  # fix for PyQt5/6
    QtCore.QDateTime.toPython = QtCore.QDateTime.toPyDateTime

运行 使用 PyQt5 或 PyQt6,这导致

TypeError: toPyDateTime(self): first argument of unbound method must have type 'QDateTime'

调用函数时:

QtCore.QDateTime.currentDateTime().toPython()

但是如果我把调用改成

QtCore.QDateTime.toPython(QtCore.QDateTime.currentDateTime())

没有错误。

但是,当我把第一段代码改成

if not hasattr(QtCore.QDateTime, 'toPython'):  # fix for PyQt5/6
    QtCore.QDateTime.toPython = lambda self: QtCore.QDateTime.toPyDateTime(self)

无论我如何调用 toPython 函数,一切正常。为什么我在这里需要 lambda 表达式?

已添加。 为了解释我期望的行为,有一段简单的代码:

class A:
    def __init__(self) -> None:
        print(f'make A ({hex(id(self))})')

    def foo(self) -> None:
        print(f'A ({hex(id(self))}) foo')


class B:
    def __init__(self) -> None:
        print(f'make B ({hex(id(self))})')


B.bar = A.foo

b: B = B()  # prints “make B (0x7efc04c67f10)” (or another id)
B.bar(b)    # prints “A (0x7efc04c67f10) foo” (same id, no error)
b.bar()     # same result as before, no error

相反,下面的代码不起作用:

from PyQt6.QtCore import QDateTime

QDateTime.toPython = QDateTime.toPyDateTime
t: QDateTime = QDateTime.currentDateTime()
QDateTime.toPython(t)  # no error
t.toPython()           # raises TypeError

这是由于 PyQt 和 PySide 之间的实现差异。在前者中,类 的大多数方法都是围绕 C-functions 的薄包装器,它们没有实现 descriptor protocol(即它们没有 __get__ 方法)。因此,在这方面,它们等同于 built-in 函数,例如 len:

>>> type(len)
<class 'builtin_function_or_method'>
>>> type(QtCore.QDateTime.toPyDateTime) is type(len)
True
>>> hasattr(QtCore.QDateTime.toPyDateTime, '__get__')
False

相比之下,大多数 PySide 方法 do 实现描述符协议:

>>> type(QtCore.QDateTime.toPython)
<class 'method_descriptor'>
>>> hasattr(QtCore.QDateTime.toPython, '__get__')
True

这意味着如果您取消兼容性修复,它按预期工作:

>>> from PySide2 import QtCore
QtCore.QDateTime.toPyDateTime = QtCore.QDateTime.toPython
>>> QtCore.QDateTime.currentDateTime().toPyDateTime()
datetime.datetime(2022, 4, 29, 11, 52, 51, 67000)

但是,如果您想保留当前的命名方案,使用包装函数(例如 lambda)本质上是最好的选择。所有 user-defined Python 函数都支持描述符协议,这就是为什么您的示例使用简单 user-defined 类 的行为符合预期。可能建议的唯一改进是改用 partialmethod。这将节省编写一些 boiler-plate 代码,并具有提供更多信息的额外好处 error-messages:

>>> QtCore.QDateTime.toPython = partialmethod(QtCore.QDateTime.toPyDateTime)
>>> d = QtCore.QDateTime.currentDateTime()
>>> d.toPython()
datetime.datetime(2022, 4, 29, 12, 13, 15, 434000)
>>> d.toPython(42)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3.10/functools.py", line 388, in _method
    return self.func(cls_or_self, *self.args, *args, **keywords)
TypeError: toPyDateTime(self): too many arguments

我想这里唯一剩下的一点就是为什么 PyQt 和 PySide 在实现上完全不同的问题。您可能需要 ask the author of PyQt 才能得到关于此的明确解释。我的猜测是,这至少部分是出于历史原因,因为 PyQt 的存在时间比 PySide 长得多——但毫无疑问还有其他一些技术考虑因素。