Python的观察者模式,PyQt 信号和插槽 机制的理解和应用
介绍一下Python语言下的观察者模式和其在PyQt运用
内容介绍
- 提供信号插槽的python实现方式(更大的灵活性)
- 修改PyQt版的信号和插槽机制使slot可以接受到关键字参数
信号与插槽的 Python 实现
介绍
- 因为实现机制的是Python代码不是PyQt的c++,效率可能会更低
- 更贴合Python的风格,more pythonic,限制更少
1. 不能用实例属性替换pyqtSignal
的类属性
..
虽然 pySignal
起源于 pyqtSignal
而且直接实例化 pySignal
类的各种使用方式和 pyqtSignal
相同(毕竟设计的时候就接口相同),但是有个容易让人困惑的点是
1 | # 代码示例,pySignal 在实例化后的类中替换 pyqtSignal |
常理来说 pyqtSignal()
应该被 pySignal
替换掉了,但如早先提到的 pyqtSignal
在所在类实例化后摇身一变成实例化的PyQt5.QtCore.pyqtBoundSignal
对象。
这代表我们先赋值的类属性已经变成了实例化类的属性了
这样造成的影响是,如果我们给某类多个实例,它们将有自己的signal而且互不干扰(emit 也不干扰)
我们实现的类没有这样的特性,类属性一直是类属性
这意味着给某类赋值类属性,其实例也会遗传相同的类属性(不独立)
解决方案是,pySignal在最开始就应该是实例属性而不是类属性(这样能保证独立)
pySignal
1 | class Signal: |
PyQt signal&slot
介绍
1. signal只能作为类属性 Only works with class attributes
Signals are not class attributes. PyQt5.QtCore.pyqtSignal()
is merely a vessel for a future instance variable containing a PyQt*.QtCore.pyqtBoundSignal instance. When you instantiate your class, pyqtSignal goes to work and injects itself as an instance variable and adds itself to the QMetaObject of the class.
QMetaObject? It comes with useful methods such as .className(), superClass(), methodCount() which returns the name of the class, its superclasses and number of methods respectively.
In C++ these are probably very useful, however a Python programmer might not be very impressed. It’s something we’ve had access to all along via any instances’ class, bases and __dict__attributes.
2.不能在已经实例化的类中声明 Cannot be used in an already instantiated class
这是最让人头痛的特性 Now here’s the kicker.
If you’re doing any sort of base- or abstract class work with Qt widgets, you’ll quickly realise that you can’t inherit signals.
Other than that, if try and bypass inheritance and have a builder spit out widgets for you, you’ll also notice how Dependency Injection isn’t going to work with signals. They have to be created as class attributes and they can only be created using pyqtSignal(). Please correct me if I’m wrong.
3.必须在声明signal时指明传输参数的类型 Must be pre-specified with any data-types you wish to emit
类似于强类型语言的类型声明,这完全不是Python的风格嘛。
In other languages, this is referred to as static typing. Python however doesn’t do any of that.
1 | # 这是演示代码(伪代码),实际上得作为类属性声明 |
4. 不支持关键字参数 Does not support keyword arguments
TypeError: emit() takes no keyword arguments
Keyword arguments are quite useful as a means of self-documenting code.
signal.emit(5)
本应该写做signal.emit(velocity=5)
这样不仅增强可读性,还可以加强机制使其可以携带相同关键字参数 it can also be used to enforce signals and slots to carry an identical argument signature.
1 | def callback(name, address): |
5. Cannot be modified after instantiation
Python对象,很自然的支持 代码动态修改(monkey-patch),但pyqtSignals
非常特别的不支持
As a Python object, you would expect the ability to monkey-patch, but pyqtSignals are special enough to not let you do any of that.
I’ll provide an example of monkey-patching for you below.
理解 Understanding Signals and Slots
Qt 提供的 信号和槽 机制基于一个(行为型)设计模式 ->观察者模式 (Observer pattern) (TODO 另开一文详细分析 Python的观察者模式)
跨线程
上述实现很直接的介绍了基本原理但缺少了很多应该考虑到的应用场景比如跨线程。
在使用pySignal时你可能已经遇到slot所在的线程很容易崩溃(无论是在 QThread
或者 Python的多线程模块),这“怪罪于”多线程之美,两个线程可能同时尝试或访问同一资源.
QObject.sender()
简而言之, QObject.sender()
可以让接收端可以获得发送端资源。
1 | def callback(self, message): |
API参考资料中警告说这种方式可能会破坏面向对象程序的模块性,建议尽量避免
在这里我不准备提供全方位的测评, 但py版相比qt版 pyqtSignal()
, 如果是obj1 -> obj2 -> obj3 这种链式调用的话,在obj3的slot中查询sender的话会返回obj1
1 |
|
应用
Example – 监视类属性的状态
It can sometimes be useful to monitor an attribute of a class.
1 | class Listener(object): |
总结
- 在某些复杂的场景下,即使功能强大的
pyqtSignal
也会触短板,这个时候就可以扩展或自己实现
- 但选择不站在巨人肩膀上而自己实现某一特性的话,有些情况我们会在遇到时发现(像先提到的线程间的安全性)。这个时候就不得不挑起自己动手实现新特性的担子。
- 虽然两种signal机制的实现目的是一样的,但它们不一定需要用谁来替换谁。可能最好的方式是两个在代码中同时使用,各取所长,在各自适合的应用场景使用。
例如,QThreads
可以使用pyqtSignal
而我们自定义的组件的基类和制造器(TODO builders)就可以使用pySignal
相关资源
- 来源: 英文 多样化信号
- 资料: PyQt5 官方文档
- 书籍: Design Patterns
Is an excellent summary and reference of many very useful patterns. - 书籍: Head First Design Patterns
Provides a more gentle and explanatory view of many of the same patterns. - 讨论: SOF Custom pyqtSignal implementation