Система подій в Python


196

Яку систему подій для Python ви використовуєте? Мені вже відомий підиспатчер , але мені було цікаво, що ще можна знайти, або зазвичай використовується?

Мене не цікавлять менеджери подій, які є частиною великих фреймворків, я б скоріше скористався невеликим рішенням з голими кістками, яке я легко розширюю.

Відповіді:


180

Пакети PyPI

Станом на червень 2020 року, це пакети, пов’язані з подіями, доступні на PyPI, упорядковані до останньої дати випуску.

Є ще більше

Це багато бібліотек для вибору, використовуючи дуже різну термінологію (події, сигнали, обробники, відправлення методів, гачки, ...).

Я намагаюся зберегти огляд вищевказаних пакетів, а також методи, згадані у відповідях тут.

По-перше, деяка термінологія ...

Шаблон спостерігача

Найбільш базовий стиль системи подій - це «мішок методів обробника», який є простою реалізацією шаблону спостерігача .

В основному, оброблювальні методи (дзвінки) зберігаються в масиві і кожен викликається, коли подія "запускається".

Опублікувати-Підписатися

Недоліком систем подій Observer є те, що ви можете реєструвати обробники лише у фактичному об'єкті події (або у списку обробників). Тож у час реєстрації подія вже має існувати.

Ось чому існує другий стиль систем подій: схема публікації-підписки . Тут обробники реєструються не в об'єкті події (або в списку обробників), а в центральному диспетчері. Також сповіщувачі спілкуються лише з диспетчером. Що слухати чи що публікувати, визначається «сигналом», що є не що інше, як ім’я (рядок).

Шаблон посередника

Можливо, це також буде цікавим: модель посередника .

Гачки

Система "гачок" зазвичай використовується в плагінах додатків. Додаток містить фіксовані точки інтеграції (гачки), і кожен плагін може підключитися до цього гака та виконати певні дії.

Інші "події"

Примітка: threading.Event не є «системою подій» у вищезгаданому сенсі. Це система синхронізації потоків, де один потік чекає, поки інший потік «сигналізує» об’єкт події.

Мережеві бібліотеки обміну повідомленнями часто також використовують термін "події"; іноді вони подібні за концепцією; іноді ні. Звичайно, вони можуть перетинати межі потоку, процесів та комп'ютера. Див., Наприклад, pyzmq , pymq , Twisted , Tornado , gevent , eventlet .

Слабкі посилання

У Python, посилання на метод або об’єкт гарантує, що він не буде видалений смітником. Це може бути бажано, але це також може призвести до витоку пам'яті: пов'язані обробники ніколи не очищаються.

Деякі системи подій використовують для вирішення цього питання слабкі посилання замість звичайних.

Деякі слова про різні бібліотеки

Системи подій у стилі спостерігача:

  • zope.event показує голі кістки, як це працює (див . відповідь Леннарта ). Примітка. Цей приклад навіть не підтримує аргументи обробника.
  • Реалізація "списку дзвінків" LongPoke показує, що така система подій може бути реалізована дуже мінімалістично шляхом підкласифікації list.
  • Варіант Felk EventHook також забезпечує підписи викликів і абонентів.
  • EventHook spassig (шаблон події Майкла Форада) - це просто реалізація.
  • Клас подій Йосипа Оцінені уроки в основному однаковий, але він використовує setзамість того, listщоб зберігати сумку, та інвентар, __call__який є і розумним доповненням.
  • PyNotify подібний за концепцією, а також надає додаткові поняття змінних та умов ("подія, змінена зміною"). Домашня сторінка не функціонує.
  • axel - це в основному мішок з великою кількістю функцій, пов’язаних із різьбленням, обробкою помилок, ...
  • python-dispatch вимагає отримання рівних класів джерел pydispatch.Dispatcher.
  • buslane заснований на класах, підтримує одно- або декілька обробників та полегшує підказки широкого типу.
  • Спостерігач / подія Pithikos - це легкий дизайн.

Публікація-підписка бібліотек:

  • blinker має деякі чудові функції, такі як автоматичне відключення та фільтрація на основі відправника.
  • PyPubSub - це стабільний пакет, який обіцяє "розширені функції, що полегшують налагодження та підтримку тем і повідомлень".
  • pymitter є портом Python Node.js EventEmitter2 і пропонує простори імен, макіяж та TTL.
  • Здається, PyDispatcher підкреслює гнучкість щодо публікації багатьох до багатьох тощо. Підтримує слабкі посилання.
  • louie - це перероблений PyDispatcher і повинен працювати "в найрізноманітніших контекстах".
  • pypydispatcher заснований на (ви здогадалися ...) PyDispatcher, а також працює в PyPy.
  • django.dispatch - це переписаний PyDispatcher "з більш обмеженим інтерфейсом, але більш високою продуктивністю".
  • pyeventdispatcher заснований на диспетчері подій PHP в Symfony.
  • диспетчер був вилучений з django.dispatch, але він стає досить старим.
  • Крістіан Гарсія EventManger - це дійсно коротка реалізація.

Інші:

  • pluggy містить систему гака, яка використовується pytestплагінами.
  • RxPy3 реалізує шаблон "Спостереження" і дозволяє об'єднувати події, повторити і т.д.
  • Сигнали та слоти Qt доступні у PyQt або PySide2 . Вони працюють як зворотний виклик, коли вони використовуються в одному потоці або як події (використовуючи цикл подій) між двома різними потоками. Сигнали та слоти мають обмеження, що вони працюють лише в об'єктах класів, які походять від QObject.

2
Також є louie, заснована на PyDispatcher: pypi.python.org/pypi/Louie/1.1
the979kid

@ the979kid louie, здається, погано підтримується, сторінка pypi посилається на 404s на GitHub: 11craft.github.io/louie ; github.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, яка, можливо, добре підходить для цього списку.
Четвер

100

Я робив це так:

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)

Однак, як і у всьому іншому, що я бачив, для цього немає автоматичного генерованого підока, а також підписів, який справді смокче.


3
Я вважаю цей стиль досить інтригуючим. Це солодко голі кістки. Мені подобається те, що це дозволяє маніпулювати подіями та їхніми підписниками як автономні операції. Я побачу, як це проходить в реальному проекті.
Руді Латта

2
Дуже гарний мінімалістичний стиль! супер!
akaRem

2
Я цього не можу підкреслити, це справді просто і просто.

2
велика прихильність, хтось може пояснити це так, як мені було 10? Чи успадковується цей клас основним класом? Я не бачу init, щоб супер () не використовувався. Мені це чомусь не клацає.
omgimdrunk

1
@omgimdrunk Простий обробник подій запускає одну або кілька функцій, що викликаються, кожного разу, коли подія починається. Клас, який "керує" цим для вас, зажадає як мінімум наступних методів - add & fire. У межах цього класу вам потрібно буде підтримувати список обробників, які потрібно виконати. Поставимо це до змінної екземпляра, _bag_of_handlersяка є списком. Метод додавання класу був би просто self._bag_of_handlers.append(some_callable). Метод вогню класу буде циклічно переходити через _ _______обробників ', передаючи надані аргументи та кварги на оброблювачі, і виконувати кожну послідовно.
Гейб Спрадлін

69

Ми використовуємо 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

Недоліком використання цього є те, що спочатку потрібно додати подію, перш ніж зареєструватися як підписник. Якщо лише видавці додають свої події (не обов'язково, а лише хороша практика), то ви повинні ініціалізувати видавців перед передплатниками, що болить у великих проектах
Джонатан

6
останній метод помиляється, тому що обробники self .__ змінюються під час ітерацій. Виправлення: `self .__ handlers = [h for h in self .__ обробники, якщо h.im_self! = Obj]`
Simon Bergot

1
@Simon має рацію, але вводить помилку, оскільки ми можемо мати незв'язані функції в самообробних .__ обробниках. Виправлення: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 деяку пропускну здатність ;-)
Boldewyn

Я все одно хотів би мати можливість надсилати повідомлення. Я б використовував систему подій у додатку, побудованому на Tkinter. Я не використовую систему подій, оскільки вона не підтримує повідомлення.
Йосип

Ви можете надсилати що завгодно за допомогою zope.event. Але я можу сказати, що це не належна система обміну повідомленнями, оскільки ви не можете надсилати події / повідомлення в інші процеси чи інші комп'ютери. Напевно, ви повинні бути, але більш конкретними щодо своїх вимог.
Леннарт Регебро

15

Я знайшов цей маленький сценарій на оцінених уроках . Здається, я маю правильне співвідношення простоти / потужності, за яким я домагаюся. Пітер Тетчер є автором наступного коду (жодне ліцензування не згадується).

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
Використовувати набір () замість списку приємно, щоб уникнути реєстрації обробників двічі. Одним із наслідків є те, що обробники не викликаються в порядку, в якому вони були зареєстровані. Не обов'язково погана річ, хоча ...
florisla

1
@florisla може замінити на OrdersSet, якщо так хоче.
Робіно

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')

Мені подобається ваш дизайн, він мінімалістичний і простий для розуміння. і це було б мало, якщо не потрібно імпортувати деякі модулі.
Атреягаурав

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

Ви можете подивитися на пімітер ( піпі ). Це невеликий однофайловий (~ 250 локальний) підхід "надання просторів імен, макіяжів та 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, те саме стосується і джанго

Якщо я роблю асинхронізацію I / OI, використовуйте нативний модуль вибору

Якщо я призначаю аналізатор пітона SAX, я використовую API подій, наданий SAX. Так виглядає, що я жертва основного API :-)

Можливо, ви повинні запитати себе, чого ви очікуєте від рамки / модуля подій. Мої особисті переваги - використовувати парадигму Socket / Signal від QT. Більше інформації про це можна знайти тут


2

Ось ще один модуль для розгляду. Це здається життєздатним вибором для більш вимогливих програм.

Py-notify - це пакет Python, що забезпечує інструменти для реалізації шаблону програмування Observer. Ці інструменти включають сигнали, умови та змінні.

Сигнали - це списки обробників, які викликаються, коли випромінюється сигнал. У основному умови булеві змінні, поєднані з сигналом, який випромінюється, коли стан стану змінюється. Їх можна комбінувати за допомогою стандартних логічних операторів (не, та ін.) У складні умови. Змінні, на відміну від умов, можуть містити будь-який об’єкт Python, не лише булеві, але їх не можна поєднувати.


1
Домашня сторінка не введена в експлуатацію для цієї, можливо, більше не підтримується?
Девід Паркс

1

Якщо ви хотіли робити більш складні речі, такі як об'єднання подій або повторного спроби, ви можете використовувати шаблон спостерігання та зрілу бібліотеку, яка це реалізує. 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. Версія за замовчуванням працює поверх сервера Backis, тому вам потрібен запущений сервер 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

Погляньте пієвент-диспетчер

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.