Python中的事件系统


195

您使用哪个Python事件系统?我已经知道pydispatcher,但是我想知道还能找到什么或常用的东西?

我对大型框架中的事件管理器不感兴趣,我宁愿使用可以轻松扩展的小型准系统解决方案。

Answers:


178

PyPI软件包

截至2020年6月,这些都是PyPI上与事件相关的软件包,按最新发布日期订购。

还有更多

使用非常不同的术语(事件,信号,处理程序,方法分派,hook等),可以选择很多库。

我试图对上述软件包以及此处答案中提到的技术进行概述。

首先,一些术语...

观察者模式

事件系统最基本的样式是“处理程序方法包”,它是Observer模式的简单实现

基本上,处理程序方法(可调用函数)存储在数组中,并在事件“触发”时分别调用。

发布-订阅

Observer事件系统的缺点是只能在实际的Event对象(或处理程序列表)上注册处理程序。因此,在注册时该事件已经需要存在。

这就是存在事件系统第二种样式的原因: 发布-订阅模式。在这里,处理程序不在事件对象(或处理程序列表)上注册,而是在中央调度程序上注册。通知者也仅与调度程序对话。侦听或发布的内容由“信号”确定,仅不过是名称(字符串)。

中介者模式

可能也很有趣:中介者模式

钩子

在应用程序插件的上下文中通常使用“挂钩”系统。该应用程序包含固定的集成点(挂钩),并且每个插件都可以连接到该挂钩并执行某些操作。

其他“事件”

注意:从上述意义上讲,threading.Event不是“事件系统”。这是一个线程同步系统,其中一个线程等待,直到另一个线程“标记” Event对象。

网络消息传递库也经常使用术语“事件”。有时这些在概念上是相似的;有时不是。它们当然可以遍历线程,进程和计算机的边界。参见例如 pyzmqpymqTwistedTornadogeventeventlet

参考不足

在Python中,持有对方法或对象的引用可确保不会被垃圾收集器删除。这可能是理想的,但它也可能导致内存泄漏:永远不会清除链接的处理程序。

一些事件系统使用弱引用而不是常规引用来解决此问题。

关于各种库的一些话

观察者式事件系统:

  • zope.event显示了其工作原理的基本内容(请参阅Lennart的答案)。注意:此示例甚至不支持处理程序参数。
  • LongPoke的“可调用列表”实现表明,可以通过子类化非常简单地实现这样的事件系统list
  • Felk的变体EventHook还可以确保被呼叫者和呼叫者的签名。
  • spassig的EventHook(Michael Foord的事件模式)是一个简单的实现。
  • Josip的Valued Lessons Event类基本上是相同的,但是使用a set而不是a list来存储包,并且实现__call__都是合理的补充。
  • PyNotify在概念上相似,还提供了变量和条件(“变量更改事件”)的其他概念。主页不起作用。
  • axel基本上是一个处理程序包,具有更多与线程,错误处理,...相关的功能。
  • python-dispatch需要偶数源类从派生pydispatch.Dispatcher
  • buslane是基于类的,支持单个或多个处理程序,并有助于扩展类型提示。
  • Pithikos的Observer / Event是轻巧的设计。

发布-订阅库:

  • 方向指示灯具有一些漂亮的功能,例如自动断开连接和基于发件人的过滤。
  • PyPubSub是一个稳定的软件包,并承诺“有助于调试和维护主题和消息的高级功能”。
  • pymitter是Node.js EventEmitter2的Python端口,并提供名称空间,通配符和TTL。
  • PyDispatcher似乎在多对多出版物等方面强调灵活性。支持弱引用。
  • 路易是一个经过改进的PyDispatcher,并应努力“在各种各样的环境中”。
  • pypydispatcher基于(您猜对了……)PyDispatcher,并且也可以在PyPy中使用。
  • django.dispatch是重写的PyDispatcher,“界面更有限,但性能更高”。
  • pyeventdispatcher基于PHP的Symfony框架的event-dispatcher。
  • 调度程序是从django.dispatch中提取的,但是已经相当老了。
  • Cristian Garcia的EventManger是一个非常短的实现。

其他:

  • Pluggy包含一个pytest插件使用的挂钩系统。
  • RxPy3实现了Observable模式,并允许合并事件,重试等。
  • Qt的信号和插槽可从PyQtPySide2获得。当在同一线程中使用时,它们可用作回调,或在两个不同线程之间用作事件(使用事件循环)。信号和插槽的局限性是它们仅在从派生的类的对象中工作QObject

2
还有基于PyDispatcher的louie:pypi.python.org/pypi/Louie/1.1
the979kid 2015年

@ the979kid louie似乎维护得不好,pypi页面链接到GitHub上的404s11craft.github.io/louiegithub.com/gldnspud/louie。应该是github.com/11craft/louie
florisla

1
弱引用事件监听器是常见的需求。否则,现实世界的使用将变得艰巨。说明哪些解决方案可能有用。
kxr

Pypubsub 4是多对多的,并具有强大的消息调试工具以及多种约束消息有效负载的方式,因此您可以更早地知道发送无效数据或丢失数据的时间。PyPubSub 4支持Python 3(而PyPubSub 3.x支持Python 2)。
奥利弗

我最近发布了一个名为pymq github.com/thrau/pymq的库,它可能很适合此列表。
thrau

98

我一直这样:

class Event(list):
    """Event subscription.

    A list of callable objects. Calling an instance of this will cause a
    call to each item in the list in ascending order by index.

    Example Usage:
    >>> def f(x):
    ...     print 'f(%s)' % x
    >>> def g(x):
    ...     print 'g(%s)' % x
    >>> e = Event()
    >>> e()
    >>> e.append(f)
    >>> e(123)
    f(123)
    >>> e.remove(f)
    >>> e()
    >>> e += (f, g)
    >>> e(10)
    f(10)
    g(10)
    >>> del e[0]
    >>> e(2)
    g(2)

    """
    def __call__(self, *args, **kwargs):
        for f in self:
            f(*args, **kwargs)

    def __repr__(self):
        return "Event(%s)" % list.__repr__(self)

但是,就像我看到的所有其他内容一样,没有为此自动生成的pydoc,也没有签名,这确实很糟糕。


3
我觉得这种风格很有趣。简直太棒了。我喜欢这样一个事实,它允许人们以自治操作的方式操纵事件及其订阅者。我将了解它在实际项目中的表现。
鲁迪·拉塔

2
非常漂亮的简约风格!超!
akaRem 2013年

2
我不能对此表示足够的赞同,这确实非常简单。

2
大忙,有人可以像我10岁时这样解释吗?此类是否被主类继承?我没有看到init,所以不会使用super()。由于某种原因,它不是为我点击。
omgimdrunk

1
@omgimdrunk一个简单的事件处理程序将在事件触发时触发一个或多个可调用函数。一个为您“管理”此类的类至少需要以下方法-添加并触发。在该类中,您需要维护要执行的处理程序列表。让我们把它放在_bag_of_handlers一个列表的实例变量中。该类的add方法将只是self._bag_of_handlers.append(some_callable)。该类的fire方法将通过_bag_of_handlers`循环,将提供的args和kwargs传递给处理程序,并依次执行每个处理程序。
·斯普拉德林

68

我们使用Michael Foord在他的Event Pattern中建议的EventHook :

只需使用以下命令将EventHooks添加到您的类中:

class MyBroadcaster()
    def __init__():
        self.onChange = EventHook()

theBroadcaster = MyBroadcaster()

# add a listener to the event
theBroadcaster.onChange += myFunction

# remove listener from the event
theBroadcaster.onChange -= myFunction

# fire event
theBroadcaster.onChange.fire()

我们添加了将所有侦听器从一个对象删除到Michaels类的功能,并最终得到了以下结果:

class EventHook(object):

    def __init__(self):
        self.__handlers = []

    def __iadd__(self, handler):
        self.__handlers.append(handler)
        return self

    def __isub__(self, handler):
        self.__handlers.remove(handler)
        return self

    def fire(self, *args, **keywargs):
        for handler in self.__handlers:
            handler(*args, **keywargs)

    def clearObjectHandlers(self, inObject):
        for theHandler in self.__handlers:
            if theHandler.im_self == inObject:
                self -= theHandler

使用此功能的一个缺点是您需要先注册一个事件,然后才能注册为订阅者。如果只有发布者添加事件(不是必须的,这只是一个好习惯),那么您必须在订阅者之前初始化发布者,这在大型项目中很痛苦
Jonathan

6
最后一个方法有问题,因为在迭代过程中修改了self .__ handlers。修正:`self .__ handlers = [h for self .__ handlers if h.im_self!= obj]`
Simon Bergot 2013年

1
@Simon是正确的,但是引入了一个错误,因为我们可以在self .__ handlers中具有未绑定的函数。修正:self.__handlers = [h for h in self._handlers if getattr(h, 'im_self', False) != obj]
埃里克·马科斯

20

我使用zope.event。这是您可以想象的最裸露的骨头。:-)实际上,这是完整的源代码:

subscribers = []

def notify(event):
    for subscriber in subscribers:
        subscriber(event)

请注意,例如,您不能在进程之间发送消息。它不是消息系统,只是事件系统,仅此而已。


17
pypi.python.org/pypi/zope.event ...为可怜的Google救了一些
乐队

我仍然希望能够发送消息。我会在基于Tkinter的应用程序中使用事件系统。我不使用它的事件系统,因为它不支持消息。
Josip)

您可以使用zope.event发送任何内容。但是我的观点是,这不是一个适当的消息传递系统,因为您无法将事件/消息发送到其他进程或其他计算机。您可能应该对您的要求有所了解。
Lennart Regebro

15

我在“ 有价值的课程”中找到了这个小脚本。我似乎拥有正确的简单性/功率比。Peter Thatcher是以下代码的作者(未提及许可)。

class Event:
    def __init__(self):
        self.handlers = set()

    def handle(self, handler):
        self.handlers.add(handler)
        return self

    def unhandle(self, handler):
        try:
            self.handlers.remove(handler)
        except:
            raise ValueError("Handler is not handling this event, so cannot unhandle it.")
        return self

    def fire(self, *args, **kargs):
        for handler in self.handlers:
            handler(*args, **kargs)

    def getHandlerCount(self):
        return len(self.handlers)

    __iadd__ = handle
    __isub__ = unhandle
    __call__ = fire
    __len__  = getHandlerCount

class MockFileWatcher:
    def __init__(self):
        self.fileChanged = Event()

    def watchFiles(self):
        source_path = "foo"
        self.fileChanged(source_path)

def log_file_change(source_path):
    print "%r changed." % (source_path,)

def log_file_change2(source_path):
    print "%r changed!" % (source_path,)

watcher              = MockFileWatcher()
watcher.fileChanged += log_file_change2
watcher.fileChanged += log_file_change
watcher.fileChanged -= log_file_change2
watcher.watchFiles()

1
使用set()而不是列表可以避免处理程序被两次注册。结果之一是,未按注册的顺序调用处理程序。虽然不一定坏事……
florisla

1
如果需要的话,@ florisla可以换成OrderedSet。
罗宾诺

9

这是一个应该可以正常工作的最小设计。您要做的就是简单地Observer在一个类中继承,然后使用observe(event_name, callback_fn)它侦听特定事件。每当该特定事件在代码中的任意位置(即Event('USB connected'))触发时,就会触发相应的回调。

class Observer():
    _observers = []
    def __init__(self):
        self._observers.append(self)
        self._observed_events = []
    def observe(self, event_name, callback_fn):
        self._observed_events.append({'event_name' : event_name, 'callback_fn' : callback_fn})


class Event():
    def __init__(self, event_name, *callback_args):
        for observer in Observer._observers:
            for observable in observer._observed_events:
                if observable['event_name'] == event_name:
                    observable['callback_fn'](*callback_args)

例:

class Room(Observer):
    def __init__(self):
        print("Room is ready.")
        Observer.__init__(self) # DON'T FORGET THIS
    def someone_arrived(self, who):
        print(who + " has arrived!")

# Observe for specific event
room = Room()
room.observe('someone arrived',  room.someone_arrived)

# Fire some events
Event('someone left',    'John')
Event('someone arrived', 'Lenard') # will output "Lenard has arrived!"
Event('someone Farted',  'Lenard')

我喜欢您的设计,它简约且易于理解。而且由于不必导入某些模块,因此重量很轻。
Atreyagaurav

8

我创建了一个EventManager类(最后是代码)。语法如下:

#Create an event with no listeners assigned to it
EventManager.addEvent( eventName = [] )

#Create an event with listeners assigned to it
EventManager.addEvent( eventName = [fun1, fun2,...] )

#Create any number event with listeners assigned to them
EventManager.addEvent( eventName1 = [e1fun1, e1fun2,...], eventName2 = [e2fun1, e2fun2,...], ... )

#Add or remove listener to an existing event
EventManager.eventName += extra_fun
EventManager.eventName -= removed_fun

#Delete an event
del EventManager.eventName

#Fire the event
EventManager.eventName()

这是一个例子:

def hello(name):
    print "Hello {}".format(name)
    
def greetings(name):
    print "Greetings {}".format(name)

EventManager.addEvent( salute = [greetings] )
EventManager.salute += hello

print "\nInitial salute"
EventManager.salute('Oscar')

print "\nNow remove greetings"
EventManager.salute -= greetings
EventManager.salute('Oscar')

输出:

最初的致敬
问候奥斯卡
你好奥斯卡

现在删除问候
你好奥斯卡

EventManger代码:

class EventManager:
    
    class Event:
        def __init__(self,functions):
            if type(functions) is not list:
                raise ValueError("functions parameter has to be a list")
            self.functions = functions
            
        def __iadd__(self,func):
            self.functions.append(func)
            return self
            
        def __isub__(self,func):
            self.functions.remove(func)
            return self
            
        def __call__(self,*args,**kvargs):
            for func in self.functions : func(*args,**kvargs)
            
    @classmethod
    def addEvent(cls,**kvargs):
        """
        addEvent( event1 = [f1,f2,...], event2 = [g1,g2,...], ... )
        creates events using **kvargs to create any number of events. Each event recieves a list of functions,
        where every function in the list recieves the same parameters.
        
        Example:
        
        def hello(): print "Hello ",
        def world(): print "World"
        
        EventManager.addEvent( salute = [hello] )
        EventManager.salute += world
        
        EventManager.salute()
        
        Output:
        Hello World
        """
        for key in kvargs.keys():
            if type(kvargs[key]) is not list:
                raise ValueError("value has to be a list")
            else:
                kvargs[key] = cls.Event(kvargs[key])
        
        cls.__dict__.update(kvargs)

8

您可以看看pymitterpypi)。它是一种小的单文件(〜250 loc)方法,“提供名称空间,通配符和TTL”。

这是一个基本示例:

from pymitter import EventEmitter

ee = EventEmitter()

# decorator usage
@ee.on("myevent")
def handler1(arg):
   print "handler1 called with", arg

# callback usage
def handler2(arg):
    print "handler2 called with", arg
ee.on("myotherevent", handler2)

# emit
ee.emit("myevent", "foo")
# -> "handler1 called with foo"

ee.emit("myotherevent", "bar")
# -> "handler2 called with bar"

6

我对Longpoke的简约方法进行了改进,该方法还确保了被叫者和呼叫者的签名:

class EventHook(object):
    '''
    A simple implementation of the Observer-Pattern.
    The user can specify an event signature upon inizializazion,
    defined by kwargs in the form of argumentname=class (e.g. id=int).
    The arguments' types are not checked in this implementation though.
    Callables with a fitting signature can be added with += or removed with -=.
    All listeners can be notified by calling the EventHook class with fitting
    arguments.

    >>> event = EventHook(id=int, data=dict)
    >>> event += lambda id, data: print("%d %s" % (id, data))
    >>> event(id=5, data={"foo": "bar"})
    5 {'foo': 'bar'}

    >>> event = EventHook(id=int)
    >>> event += lambda wrong_name: None
    Traceback (most recent call last):
        ...
    ValueError: Listener must have these arguments: (id=int)

    >>> event = EventHook(id=int)
    >>> event += lambda id: None
    >>> event(wrong_name=0)
    Traceback (most recent call last):
        ...
    ValueError: This EventHook must be called with these arguments: (id=int)
    '''
    def __init__(self, **signature):
        self._signature = signature
        self._argnames = set(signature.keys())
        self._handlers = []

    def _kwargs_str(self):
        return ", ".join(k+"="+v.__name__ for k, v in self._signature.items())

    def __iadd__(self, handler):
        params = inspect.signature(handler).parameters
        valid = True
        argnames = set(n for n in params.keys())
        if argnames != self._argnames:
            valid = False
        for p in params.values():
            if p.kind == p.VAR_KEYWORD:
                valid = True
                break
            if p.kind not in (p.POSITIONAL_OR_KEYWORD, p.KEYWORD_ONLY):
                valid = False
                break
        if not valid:
            raise ValueError("Listener must have these arguments: (%s)"
                             % self._kwargs_str())
        self._handlers.append(handler)
        return self

    def __isub__(self, handler):
        self._handlers.remove(handler)
        return self

    def __call__(self, *args, **kwargs):
        if args or set(kwargs.keys()) != self._argnames:
            raise ValueError("This EventHook must be called with these " +
                             "keyword arguments: (%s)" % self._kwargs_str())
        for handler in self._handlers[:]:
            handler(**kwargs)

    def __repr__(self):
        return "EventHook(%s)" % self._kwargs_str()

3

如果我在pyQt中编写代码,则使用QT套接字/信号范例,对于Django也是一样

如果我正在执行异步I / O,请使用本机选择模块

如果我使用SAX python解析器,则使用的是SAX提供的事件API。所以看起来我是底层API的受害者:-)

也许您应该问自己,您对事件框架/模块有什么期望。我个人的喜好是使用QT的套接字/信号范例。有关更多信息,请参见此处


2

这是另一个需要考虑的模块。对于要求更高的应用程序,这似乎是一个可行的选择。

Py-notify是一个Python软件包,提供用于实现Observer编程模式的工具。这些工具包括信号,条件和变量。

信号是发出信号时调用的处理程序列表。条件基本上是布尔变量,加上条件状态更改时发出的信号。可以使用标准逻辑运算符(非和等)将它们组合为复合条件。与条件不同,变量可以保存任何Python对象,不仅是布尔值,而且不能将它们组合在一起。


1
主页对此页面已失效,也许不再受支持了?
David Parks

1

如果您想做更复杂的事情,例如合并事件或重试,则可以使用Observable模式和一个成熟的库来实现。https://github.com/ReactiveX/RxPY。可观察变量在Javascript和Java中非常常见,对于某些异步任务非常方便使用。

from rx import Observable, Observer


def push_five_strings(observer):
        observer.on_next("Alpha")
        observer.on_next("Beta")
        observer.on_next("Gamma")
        observer.on_next("Delta")
        observer.on_next("Epsilon")
        observer.on_completed()


class PrintObserver(Observer):

    def on_next(self, value):
        print("Received {0}".format(value))

    def on_completed(self):
        print("Done!")

    def on_error(self, error):
        print("Error Occurred: {0}".format(error))

source = Observable.create(push_five_strings)

source.subscribe(PrintObserver())

输出

Received Alpha
Received Beta
Received Gamma
Received Delta
Received Epsilon
Done!

1

如果您需要跨流程或网络边界运行的事件总线,则可以尝试PyMQ。它目前支持发布/订阅,消息队列和同步RPC。默认版本在Redis后端上运行,因此您需要运行的Redis服务器。还有一个用于测试的内存后端。您也可以编写自己的后端。

import pymq

# common code
class MyEvent:
    pass

# subscribe code
@pymq.subscriber
def on_event(event: MyEvent):
    print('event received')

# publisher code
pymq.publish(MyEvent())

# you can also customize channels
pymq.subscribe(on_event, channel='my_channel')
pymq.publish(MyEvent(), channel='my_channel')

要初始化系统:

from pymq.provider.redis import RedisConfig

# starts a new thread with a Redis event loop
pymq.init(RedisConfig())

# main application control loop

pymq.shutdown()

免责声明:我是这个图书馆的作者


0

您可以尝试buslane模块。

该库使基于消息的系统的实现更加容易。它支持命令(单个处理程序)和事件(0或多个处理程序)方法。Buslane使用Python类型注释正确注册处理程序。

简单的例子:

from dataclasses import dataclass

from buslane.commands import Command, CommandHandler, CommandBus


@dataclass(frozen=True)
class RegisterUserCommand(Command):
    email: str
    password: str


class RegisterUserCommandHandler(CommandHandler[RegisterUserCommand]):

    def handle(self, command: RegisterUserCommand) -> None:
        assert command == RegisterUserCommand(
            email='john@lennon.com',
            password='secret',
        )


command_bus = CommandBus()
command_bus.register(handler=RegisterUserCommandHandler())
command_bus.execute(command=RegisterUserCommand(
    email='john@lennon.com',
    password='secret',
))

要安装buslane,只需使用pip:

$ pip install buslane

0

前段时间,我编写了可能对您有用的库。它允许您拥有本地和全局侦听器,多种不同的注册方式,执行优先级等。

from pyeventdispatcher import register

register("foo.bar", lambda event: print("second"))
register("foo.bar", lambda event: print("first "), -100)

dispatch(Event("foo.bar", {"id": 1}))
# first second

看看pyeventdispatcher

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.