是否可以在运行时在实例上创建 pyqtSignals 而无需使用 class 变量?

Is it possible to create pyqtSignals on instances at runtime without using class variables?

是否有可能在需要时在运行时创建信号?

我正在函数中做这样的事情:

class WSBaseConnector(QObject)

    def __init__(self) -> None:
        super(QObject, self).__init__()    
        self._orderBookListeners: Dict[str, pyqtSignal[OrderBookData]] = {}

    def registerOrderBookListener(self, market: str, listener: Callable[[OrderBookData], None], loop: AbstractEventLoop) -> None:
            try:
                signal = self._orderBookListeners[market]
            except KeyError:
                signal = pyqtSignal(OrderBookData)
                signal.connect(listener)
                self._orderBookListeners[market] = signal
            else:
                signal.connect(listener)

如您所见,我有一个存储 str、pyqtSignal 对的字典。当我尝试将信号连接到监听器时,出现错误:

'PyQt5.QtCore.pyqtSignal' object has no attribute 'connect'

如果没有 class 变量,是否无法在运行时创建 pyqtSignals?

干杯。

不,这是不可能的。 pyqtSignal object is a factory function that returns a descriptor,所以它必须在执行class语句时创建。引用文档:

New signals should only be defined in sub-classes of QObject. They must be part of the class definition and cannot be dynamically added as class attributes after the class has been defined.

New signals defined in this way will be automatically added to the class’s QMetaObject. This means that they will appear in Qt Designer and can be introspected using the QMetaObject API. [emphasis added]

您的代码正在创建 unbound 信号对象,这就是您收到属性错误的原因。绑定和未绑定信号之间的区别与 classes 的方法完全相同。再次引用文档:

A signal (specifically an unbound signal) is a class attribute. When a signal is referenced as an attribute of an instance of the class then PyQt5 automatically binds the instance to the signal in order to create a bound signal. This is the same mechanism that Python itself uses to create bound methods from class functions.

据我所知,

@ekhumoro 接受的答案是完全准确的(特别是声明:“它们必须是 class 定义的一部分,不能动态添加为 [=定义 class 之后的 39=] 属性。").

不幸的是,我看到这个答案 mis-interpreted 是“不可能以编程方式生成 Qt 信号”,当然,当涉及到 Python 时,这是一个完全不同的命题。

所以恐怕这个答案没有解决原来的问题,它真的想在运行时添加信号,而不是我我以为我会澄清事实并提供一个以编程方式创建信号的示例,而不与上述声明相矛盾。解决方案是创建一个动态生成的 class 以及一些动态生成的信号。在 Python:

中有几种动态生成 classes 的好方法

选项 1 - 使用类型函数

from PyQt5.QtCore import QObject, pyqtSignal
from PyQt5.QtCore import QMetaObject


DynamicSignal = type("DynamicSignal", (QObject, ), {
    "my_custom_signal": pyqtSignal([str]),
})


if __name__ == '__main__':
    dynamic = DynamicSignal()
    dynamic.my_custom_signal.connect(print)

    dynamic.my_custom_signal.emit("Hello world")

执行时打印“Hello world”。

选项 2 - 使用元classes

我们也可以通过 metaclass:

from PyQt5.QtCore import QObject, pyqtSignal
from PyQt5.QtCore import QMetaObject


class DynamicSignalMeta(type(QObject)):
    def __new__(cls, name, bases, dct):
        dct['my_custom_signal'] = pyqtSignal([str])
        return super().__new__(cls, name, bases, dct)


class DynamicSignal(QObject, metaclass=DynamicSignalMeta):
    pass


if __name__ == '__main__':
    dynamic = DynamicSignal()
    dynamic.my_custom_signal.connect(print)

    dynamic.my_custom_signal.emit("Hello world")

在我的 中,我关注的问题是“是否可以通过编程方式添加信号”,而不是 OP 提出的问题“是否可以在运行时动态添加信号 (即在 class 实例化之后)".

相反,我认为实际上可以在运行时添加信号,尽管 PyQT 文档有非常明确的声明:

They must be part of the class definition and cannot be dynamically added as class attributes after the class has been defined

虽然我不怀疑该陈述的准确性,但 Python 是一种奇妙的动态语言,它 in-fact 相当容易达到预期的结果。我们必须克服的问题是,为了在运行时添加信号,我们必须创建一个新的 class 定义并修改实例的底层 class。在 Python 中,这可以通过设置对象的 __class__ 属性(通常 has a number of issues to be aware of)来实现。

from PyQt5.QtCore import QObject, pyqtSignal


class FunkyDynamicSignals(QObject):
    def add_signal(self, name, *args):
        # Get the class of this instance.
        cls = self.__class__

        # Create a new class which is identical to this one,
        # but which has a new pyqtSignal class attribute called of the given name.
        new_cls = type(
            cls.__name__, cls.__bases__,
            {**cls.__dict__, name: pyqtSignal(*args)},
        )
        # Update this instance's class with the newly created one.
        self.__class__ = new_cls  # noqa

有了这个 class 我们可以在实例化对象后创建信号:

>>> dynamic = FunkyDynamicSignals()
>>> dynamic.add_signal('example', [str])
>>> dynamic.example.connect(print)

>>> dynamic.add_signal('another_example', [str])
>>> dynamic.another_example.connect(print)

>>> dynamic.example.emit("Hello world")
Hello world

此方法使用现代 Python 语法(但同样可以为 Py2 编写),谨慎地公开合理的 class 层次结构并在添加新信号时保留现有连接。