Python中的懒惰事件发布订阅

0 投票
1 回答
1054 浏览
提问于 2025-04-16 01:13

我在我的谷歌应用引擎应用中需要一个事件消息系统。

我参考了以下的Python库。

http://pubsub.sourceforge.net/apidocs/concepts.html

我想问的是,我想执行的监听函数是否必须在执行路径中被导入(或者说存在)才能在事件发生时运行?

因为有很多事件,我希望尽可能地让它们按需加载。

有没有什么解决办法?

在Python中有没有懒加载的事件发布订阅框架?

1 个回答

1

tipfy 是一个专门为 App Engine 设计的小型框架,它有懒加载的功能,但只针对特定的“事件”,也就是你的代码处理的网络请求。其他一些网络框架也有这个功能,但 tipfy 足够小巧和简单,方便我们学习和模仿它的源代码。

所以,如果你找不到一个符合你口味的更丰富的事件框架,因为“懒加载”的问题,你可以选择一个需要注册/订阅可调用对象的框架,并允许用 字符串 来命名函数进行注册,就像 tipfy 那样。这样命名的函数,当然会在需要的时候及时加载,以处理某个事件。

让我用一些简化的假设代码来举个例子。假设你有一个事件框架,里面包含类似这样的内容:

import collections
servers = collections.defaultdict(list)

def register(eventname, callable):
    servers[eventname].append(callable)

def raise(eventname, *a, **k):
    for s in servers.get(eventname, ()):
        s(*a, **k)

当然,任何真实的事件框架内部会更复杂,但类似的结构在最底层是可以看得见的。

这就要求在注册时就加载可调用对象……不过,即使不去碰你框架的内部结构,你也可以轻松扩展它。想想看:

import sys

class LazyCall(object):
    def __init__(self, name):
        self.name = name
        self.f = None
    def __call__(self, *a, **k):
        if self.f is None:
            modname, funname = self.name.rsplit('.', 1)
            if modname not in sys.modules:
                __import__(modname)
            self.f = getattr(sys.modules[modname], funname)
        self.f(*a, **k)

当然,你会想要更好的错误处理等等,但这就是大致思路:把命名函数的字符串(例如 'package.module.func')封装到一个知道如何懒加载的包装对象里。现在,register(LazyCall('package.module.func')) 会在未修改的框架中注册这样一个包装器,并在请求时懒加载它。

顺便说一下,这个用例可以作为一个相当不错的 Python 习惯用法的例子,尽管一些固执的家伙大声宣称它不存在,或者不应该存在,或者其他什么:一个对象动态改变自己的类。这个习惯用法的用途是“省去中间人”,用于存在两种状态的对象,且从第一种状态到第二种状态的转变是不可逆的。在这里,懒调用者的第一种状态是“我知道函数的名字,但没有对象”,第二种状态是“我知道函数对象”。因为从第一种状态转到第二种状态是不可逆的,所以如果你愿意,可以省去每次检查的开销(或者 Strategy 设计模式的间接开销):

class _JustCallIt(object):
    def __call__(self, *a, **k):
        self.f(*a, **k)

class LazyCall(object):
    def __init__(self, name):
        self.name = name
        self.f = None
    def __call__(self, *a, **k):
        modname, funname = self.name.rsplit('.', 1)
        if modname not in sys.modules:
            __import__(modname)
        self.f = getattr(sys.modules[modname], funname)
        self.__class__ = _JustCallIt
        self.f(*a, **k)

这里的收益是微小的,因为基本上只是从每次调用中省去了一个 if self.f is None: 的检查;但这确实是一个真实的收益,除了可能让那些固执的家伙愤怒不已(如果你把 视为一个缺点)。

无论如何,具体的实现选择在于你,而不是我——或者,幸运的是,在于他们;-)。

还有一个设计选择:是否直接修改 register 使其接受字符串参数(并根据需要进行包装),基本上就像 tipfy 那样,或者在注册时进行显式包装,让 register(或者 subscribe 或其他名称)保持原样。我在这个特定情况下并不太看重“显式优于隐式”的说法,因为像这样的内容:

register(somevent, 'package.module.function')

和这样的内容:

register(somevent, LazyCall('package.module.function'))

也就是,它 确实 很清楚发生了什么,而且可以说更干净/更易读。

尽管如此,显式包装的方法确实很好,因为它不影响底层框架:在你可以传递一个函数的地方,现在你可以无缝地传递那个函数的 名字(作为字符串,命名包、模块和函数本身)。所以,如果我在改造现有框架,我会选择显式的方法。

最后,如果你想注册的可调用对象不是函数,而是某些类的实例,或者这些实例的绑定方法,你可以将 LazyCall 扩展为类似 LazyInstantiateAndCall 的变体。架构会变得稍微复杂一些,当然(因为你需要方法来实例化新对象 识别已经存在的对象,例如),但通过将这项工作委托给设计良好的工厂系统,这应该不会 糟糕。不过,我不打算深入探讨这些细节,因为这个回答已经相当长了,而且在许多情况下,简单的“命名一个函数”的方法应该就足够了!-)

撰写回答