Python中的懒惰事件发布订阅
我在我的谷歌应用引擎应用中需要一个事件消息系统。
我参考了以下的Python库。
http://pubsub.sourceforge.net/apidocs/concepts.html
我想问的是,我想执行的监听函数是否必须在执行路径中被导入(或者说存在)才能在事件发生时运行?
因为有很多事件,我希望尽可能地让它们按需加载。
有没有什么解决办法?
在Python中有没有懒加载的事件发布订阅框架?
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
的变体。架构会变得稍微复杂一些,当然(因为你需要方法来实例化新对象 和 识别已经存在的对象,例如),但通过将这项工作委托给设计良好的工厂系统,这应该不会 太 糟糕。不过,我不打算深入探讨这些细节,因为这个回答已经相当长了,而且在许多情况下,简单的“命名一个函数”的方法应该就足够了!-)