实现模块间解耦:Python消息总线实现

一、引言

在软件开发中,为了实现模块间的解耦,消息总线是一种常用的设计模式。本文将介绍如何在Python中实现消息总线,并提供具体的实现和测试代码,并说明如何将消息转发至PyQt模块。

在之前的文章中,我们已经探讨了使用Lua和C++实现消息总线的方法:

二、消息总线的基本原理

消息总线实现的基本原理如下:

  1. 被通信对象向消息总线发布一个主题。
  2. 这个主题包含消息主题和消息处理函数。
  3. 消息主题标示某个特定的主题,消息处理函数用来响应该主题的某种消息类型。
  4. 通信对象向消息总线发送某个特定主题和消息参数,总线就会根据消息主题和消息参数找到对应的消息处理函数处理该请求。

三、简单的消息总线的设计

最初的消息总线设计十分简单,使用字典来存储订阅信息,支持模块之间的同步通信。以下是原始的消息总线设计代码:

from collections import defaultdict
from threading import Lock

class PyBus:
    def __init__(self):
        self.subscriptions = defaultdict(dict)
        self.lock = Lock()

    def clear(self):
        with self.lock:
            self.subscriptions.clear()

    def subscribe(self, subject, owner, func):
        with self.lock:
            self.subscriptions[subject][owner] = func

    def unsubscribe(self, subject, owner):
        with self.lock:
            if subject in self.subscriptions and owner in self.subscriptions[subject]:
                del self.subscriptions[subject][owner]
                if not self.subscriptions[subject]:
                    del self.subscriptions[subject]

    def publish(self, subject, *args, **kwargs):
        with self.lock:
            subscribers = list(self.subscriptions.get(subject, {}).items())
        for owner, func in subscribers:
            try:
                func(*args, **kwargs)
            except Exception as e:
                print(f"Error handling {subject} for {owner}: {e}")

这个设计虽然可以工作,但存在以下几个问题:

  1. 同步处理:消息的发布和订阅均为同步操作,无法处理 I/O 密集型任务或并发任务。
  2. 缺少灵活性:无法根据任务的重要性或条件过滤消息。
  3. 错误处理简单:错误处理机制较为简陋,仅通过打印日志来处理异常。

四、使用 asyncio 进行改进

为了增强这个消息总线的能力,我们可以引入 asyncio 来支持异步处理,从而提升并发性能,特别是在 I/O 密集型场景下。此外,添加优先级和过滤器机制也能使消息处理更加灵活。以下是改进后的设计。

1. 新的消息总线设计

import asyncio
from collections import defaultdict
from threading import Lock
from functools import partial

class EventBus:
    def __init__(self):
        self.subscriptions = defaultdict(list)
        self.lock = Lock()
        self.exception_handler = None

    def clear(self):
        with self.lock:
            self.subscriptions.clear()

    def subscribe(self, subject, func, priority=0, filter_func=None):
        """订阅主题,支持优先级和过滤器"""
        with self.lock:
            self.subscriptions[subject].append({
                'handler': func,
                'priority': priority,
                'filter': filter_func
            })
            self.subscriptions[subject].sort(key=lambda x: x['priority'], reverse=True)

    def unsubscribe(self, subject, func):
        """取消订阅"""
        with self.lock:
            self.subscriptions[subject] = [
                sub for sub in self.subscriptions[subject] if sub['handler'] != func
            ]
            if not self.subscriptions[subject]:
                del self.subscriptions[subject]

    def set_exception_handler(self, handler):
        """设置异常处理器"""
        self.exception_handler = handler

    async def publish_async(self, subject, *args, **kwargs):
        """异步发布消息"""
        with self.lock:
            subscribers = list(self.subscriptions.get(subject, []))

        tasks = []
        for sub in subscribers:
            handler = sub['handler']
            filter_func = sub['filter']

            if filter_func and not filter_func(*args, **kwargs):
                continue

            if asyncio.iscoroutinefunction(handler):
                tasks.append(handler(*args, **kwargs))
            else:
                tasks.append(asyncio.to_thread(partial(handler, *args, **kwargs)))

        try:
            await asyncio.gather(*tasks)
        except Exception as e:
            if self.exception_handler:
                self.exception_handler(subject, e)
            else:
                print(f"Error handling async {subject}: {e}")

    def publish_sync(self, subject, *args, **kwargs):
        """同步发布消息"""
        with self.lock:
            subscribers = list(self.subscriptions.get(subject, []))

        for sub in subscribers:
            handler = sub['handler']
            filter_func = sub['filter']

            if filter_func and not filter_func(*args, **kwargs):
                continue

            try:
                handler(*args, **kwargs)
            except Exception as e:
                if self.exception_handler:
                    self.exception_handler(subject, e)
                else:
                    print(f"Error handling sync {subject}: {e}")

2. 主要改进点

  • 异步支持:通过 asyncio,消息总线可以在处理耗时任务时不阻塞其他任务,适用于 I/O 密集型任务(如文件操作、网络请求等)。
  • 优先级机制:在订阅消息时,允许设置优先级,高优先级的任务将先执行。
  • 过滤器机制:允许在处理消息前,根据特定条件进行过滤,只有通过过滤器的消息才会被处理。
  • 异常处理机制:用户可以自定义异常处理器,增强错误处理的灵活性。

五、使用示例

下面是如何使用改进后的消息总线的示例代码:

if __name__ == "__main__":
    START = "START"
    
    async def async_handler1():
        await asyncio.sleep(1)
        print("Async handler 1")

    async def async_handler2():
        await asyncio.sleep(1)
        print("Async handler 2")

    def sync_handler():
        print("Sync handler")

    def filter_func(*args, **kwargs):
        return kwargs.get('pass_filter', False)

    def exception_handler(subject, error):
        print(f"Error occurred in {subject}: {error}")

    # 设置异常处理器
    event_bus = EventBus()
    event_bus.set_exception_handler(exception_handler)

    # 订阅消息
    event_bus.subscribe(START, async_handler1, priority=1)
    event_bus.subscribe(START, async_handler2, priority=2)
    event_bus.subscribe(START, sync_handler, priority=0, filter_func=filter_func)

    # 测试异步发布
    asyncio.run(event_bus.publish_async(START, pass_filter=True))

    # 同步消息发布
    event_bus.publish_sync(START, pass_filter=True)

    # 取消订阅并再次发布
    event_bus.unsubscribe(START, sync_handler)
    event_bus.publish_sync(START, pass_filter=True)

在上面的代码中,我们定义了几个处理函数,其中一些是异步的(通过 async 定义),另一些是同步的。通过 asyncio.run 调用 publish_async,我们可以在异步的环境下发布消息,处理多个任务。同步的发布则通过 publish_sync 实现。


六、为什么选择 asyncio

使用 asyncio 的好处包括:

  1. 提升并发性和效率asyncio 允许在等待 I/O 操作时继续执行其他任务,避免了传统同步编程中常见的阻塞问题。
  2. 轻量级的并发模型:相比多线程或多进程,asyncio 使用协程进行调度,开销小,效率高,特别适用于 I/O 密集型任务。
  3. 更简单的并发管理:通过 await 关键字显式让出控制权,使代码更加易读和可维护。

要将改进后的消息总线模块与 PyQt 集成起来,可以通过消息总线将消息转发给 PyQt 的窗口模块。PyQt 本身有一个事件循环,而 asyncio 也有自己的事件循环,因此在集成这两者时,需要特别处理两者的事件循环协调。

关键问题:asyncio 与 PyQt 事件循环的协调

为了同时使用 PyQt 和 asyncio,我们可以使用 QEventLoop 来将 PyQt 的事件循环集成到 asyncio 中,或者使用第三方库 qasync 来简化这个过程。qasync 允许我们直接在 PyQt 应用中使用 asyncio 的功能,避免了手动处理事件循环的麻烦。

使用 qasync 集成 asyncio 和 PyQt

首先,确保你安装了 qasync

pip install qasync

七、将消息从消息总线转发到 PyQt 窗口

如果需要在发布消息时,将消息转发给 PyQt 窗口模块。接下来展示如何使用 PyQt 的 QMainWindow 和消息总线将消息传递给 PyQt 界面。

import sys
import asyncio
from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QVBoxLayout, QWidget
from qasync import QEventLoop, asyncSlot
from event_bus import EventBus  # 引入之前的消息总线模块

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("消息总线 - PyQt 示例")
        
        self.label = QLabel("等待消息...")
        
        layout = QVBoxLayout()
        layout.addWidget(self.label)
        
        container = QWidget()
        container.setLayout(layout)
        self.setCentralWidget(container)

    @asyncSlot()
    async def handle_message(self, message):
        """处理从消息总线传来的消息"""
        self.label.setText(f"收到消息: {message}")
        await asyncio.sleep(1)  # 模拟异步操作

async def main():
    # 创建 PyQt 应用
    app = QApplication(sys.argv)
    
    # 使用 QEventLoop 作为 asyncio 的事件循环
    loop = QEventLoop(app)
    asyncio.set_event_loop(loop)
    
    # 创建窗口实例
    window = MainWindow()
    window.show()
    
    # 初始化事件总线
    event_bus = EventBus()

    # 将 PyQt 的消息处理函数订阅到消息总线
    event_bus.subscribe("message_event", window.handle_message)

    # 异步发布消息
    asyncio.create_task(event_bus.publish_async("message_event", "Hello from EventBus!"))
    
    # 启动事件循环
    with loop:
        loop.run_forever()

if __name__ == "__main__":
    asyncio.run(main())

代码说明

  1. MainWindow:这是一个简单的 PyQt 主窗口,包含一个 QLabel 标签来显示从消息总线接收到的消息。
  2. handle_message 方法:通过 @asyncSlot() 装饰器,将其定义为一个可以处理异步消息的 PyQt 槽函数。当总线发布消息时,这个方法会更新标签中的内容。
  3. event_bus.subscribe:我们将 MainWindowhandle_message 方法订阅到 message_event 主题,当发布这个主题的消息时,MainWindow 将接收到并处理该消息。
  4. qasync:使用 QEventLoop 来协调 PyQt 和 asyncio 的事件循环,使得 PyQt 能够与 asyncio 异步任务一起工作。

通过集成 qasync,你可以轻松地将消息总线与 PyQt 结合起来,实现消息的异步传递与 UI 更新。这种方式使得你能够在复杂的 GUI 应用中使用消息总线,并保持异步处理的优势,特别是在处理 I/O 密集型任务时效果显著。

这样,你就可以利用消息总线在后台处理消息,同时将结果发送给 PyQt 界面进行显示或操作。

八、总结

通过引入 asyncio,我们大幅提升了消息总线的扩展性和并发处理能力,适合处理复杂的异步场景和高并发任务。新的设计还支持优先级和过滤机制,能够根据业务需求灵活地控制消息的发布和处理。

这种设计不仅提升了系统的性能,还增强了程序的灵活性和可维护性,是现代 Python 开发中一个非常实用的方案。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

橘色的喵

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值