December 9, 2014 · Python

Event Handling with Python

最近工作上常被同事問到的問題:若是現在有許多不同的 events 隨著時間傳進系統,而我們會經常新增或刪除處理這些 event 的 handlers。比較極端的情況,可能會有多個人各自 maintain 多個 modules,每個 module 都定義了一或多個 handlers,但又不希望需要修改到這個 module 以外的 code。應該要怎麼在 Python 中實作這種 event handling 的機制?

在這裡列出我想到的幾種解法,並加上我自己的一些小見解作為參考。

Handler class

一開始,同事的解法是這樣的:首先,建立一個 handler 的 base class。所有的 handler 都必須繼承自它。

# file: handler.py

from abc import ABCMeta, abstractmethod


class Handler(object):

    __metaclass__ = ABCMeta

    @abstractmethod
    def __call__(self, event):
        pass

然後在 ./handles 底下加 module,並寫上自己的 handler:

# file: handlers/foo.py

from handler import Handler


class HandlerA(Handler):

    def __call__(self, event):
        print 'handler A got event: ' + event


class HandlerB(Handler):

    def __call__(self, event):
        print 'handler B got event: ' + event


class SomethingElse(object):

    def __call__(self, event):
        print 'I\'m not a handler'

接著,取得 ./handlers 底下的 submodules 中的所有 Handler 的 subclasses:

# file: main.py

import pkgutil  
import inspect

from handler import Handler

handlers = []  
modules = pkgutil.iter_modules(path=['./handlers'])

for loader, mod_name, is_pkg in modules:  
    module = loader.find_module(mod_name).load_module(mod_name)
    for name, cls in inspect.getmembers(module, inspect.isclass):
        if issubclass(cls, Handler) and cls != Handler:
            handlers.append(cls())


def trigger_event(event):  
    for handler in handlers:
        handler(event)


if __name__ == '__main__':  
    trigger_event('hello')
    trigger_event('world')

在這裡透過 pkgutil.iter_modules() 來得到 ./handlers 底下的所有 submodules,並且利用 inspect.getmembers() 來得到每個 module 底下的所有 members。只要這個 member 是 Handler 的 subclass,就把它的 instance 「註冊」到 handlers 裡頭。

這個方法有兩個我不喜歡的地方。首先是如果 handler 並不複雜,我不會想特地為每個 handler 寫一個 class。再者,main.py 兩層迴圈裡頭的 if 條件裡必須寫著 cls != Handler 才能避免加入 handler base class(因為它只是個 abstract class)。如果我想讓某些 Handler subclasses 不被註冊(也許它是個 abstract class,或是想暫時 disable 它)呢?

handler functions

基於簡單至上原則,我希望一個 handler 只是一個普通的 function,但要怎麼區分某個 function 是不是 handler 呢?最簡單的方法就是用 function 名稱的 prefix 來區別啦:

# file: handlers/foo.py


def handler_a(event):  
    print 'handler A got event: ' + event


def handler_b(event):  
    print 'handler B got event: ' + event


def something_else(event):  
    print 'I\'m not a handler'

我們希望 handler 開頭的 function 都被加進 handlers 裡頭:

# file: main.py

import pkgutil  
import inspect

from handler import Handler

for loader, mod_name, is_pkg in modules:  
    module = loader.find_module(mod_name).load_module(mod_name)
    for name, func in inspect.getmembers(module, inspect.isfunction):
        if name.startswith('handler'):
            handlers.append(func)


def trigger_event(event):  
    for handler in handlers:
        handler(event)


if __name__ == '__main__':  
    trigger_event('hello')
    trigger_event('world')

藉由這種方法可以避免上面提到的兩個問題:現在每個 handler 都可以只用一個 function(而非 class)來實作。此外,如果我想暫時 disable 某個 handler,只要修改它的 prefix 就可以了。

Handling different types of events

上面這種解法其實已經夠好了,但讓我們稍微把情況弄複雜點。因為實際上我們還想要根據不同的 event type 決定要丟給哪些 handlers 處理。或許我們可以根據不同的 event type 制定不同的 prefix,但要如何表示能夠處理多種 event 的 handler 呢?

或許我們該走回老路:如果我們可以在用 class 代表一個 handler,就可以在 class member 裡寫下這些資訊了....

class MyHandler(Handler):

    handled_events = set(['typeX'])

    # ...
# file: main.py

import pkgutil  
import inspect

from handler import Handler

handlers = []  
modules = pkgutil.iter_modules(path=['./handlers'])

for loader, mod_name, is_pkg in modules:  
    module = loader.find_module(mod_name).load_module(mod_name)
    for name, cls in inspect.getmembers(module, inspect.isclass):
        if issubclass(cls, Handler) and cls != Handler:
            handlers.append((cls(), cls.handled_events))


def trigger_event(event):  
    for handler, handled_events in handlers:
        if event['type'] in handled_events:
            handler(event['body'])


if __name__ == '__main__':  
    trigger_event({'type': 'typeX', 'text': 'hello'})
    trigger_event({'type': 'typeY', 'text': 'world'})

等等,但剛剛提到的問題也跟著回來了:現在我還是難以控制要不要註冊某個 handler。

好吧,那把 disabled 的資訊也加到 class 裡頭呢?

# file: handler.py

class Handler(object):

    handled_events = set()
    disabled = True

    # ...
# file: main.py

# ...

for loader, mod_name, is_pkg in modules:  
    module = loader.find_module(mod_name).load_module(mod_name)
    for name, cls in inspect.getmembers(module, inspect.isclass):
        if issubclass(cls, Handler) and not cls.disabled:
            handlers.append((cls(), cls.handled_events))

# ...

不過這個 class property 會被繼承下去,所以其實每個 Handler 的 subclass 都得把 disabled 改成 False

class MyHandler(Handler):

    handled_events = set(['typeX'])
    disabled = False

    # ...

@handle_event decorator

還是老話一句,其實我不是很想為每一個 handler 寫一個 class(XD)。有個比較簡單的解法,就是利用 Python 的 decorator,把 decorated function 加進 handlers

# file: handler.py

handlers = []


def handle_event(*handled_events):  
    def register(func):
        handlers.append((func, set(handled_events)))
        return func
    return register

這裡的 handle_event 就是我們之後要拿來當 decorator 使用的。第一層 function 接收 handled_events,並在第二層拿到 handler function 的時候一併塞給 handlers

於是我們的 handlers 就可以這樣寫:

# file: handlers/foo.py

from handler import handle_event


@handle_event('typeX')
def handler_a(event):  
    print 'handler A got event: ' + event


@handle_event('typeY', 'typeZ')
def handler_b(event):  
    print 'handler B got event: ' + event

main.py 中,我們只需要 load modules 就會自動把 handlers 加進 handlers 中,就不用再掃過所有 members 一個一個檢查了:

# file: main.py

from handler import handlers

modules = pkgutil.iter_modules(path=['./handlers'])  
for loader, mod_name, is_pkg in modules:  
    loader.find_module(mod_name).load_module(mod_name)


def trigger_event(event):  
    for handler, handled_events in handlers:
        if event['type'] in handled_events:
            handler(event['text'])


if __name__ == '__main__':  
    trigger_event({'type': 'typeX', 'text': 'hello'})
    trigger_event({'type': 'typeY', 'text': 'world'})

HandlerManager class

我們還可以把註冊 handlers 這件事包裝得漂亮一點:

# handler.py

from collections import namedtuple

_Handler = namedtuple('Handler', ('handler', 'habdled_events'))


class HandlerManager(object):

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

    def register(self, handler, *handled_events):
        self._handlers.append(_Handler(handler, set(handled_events)))

    def handle_event(self, *args, **kwargs):
        def register_handler(handler):
            self.register(handler, *args, **kwargs)
            return handler
        return register_handler

    def iter_handlers(self, event):
        return (handler.handler for handler in self._handlers
                if event in handler.handled_events)


handler_manager = HandlerManager()  
handle_event = handler_manager.handle_event  

register()handlerhandled_events 包成 namedtuple 加到 self._handlers 裡頭。handle_event() 跟剛剛的實作類似:第一層 function 的 parameters 會在第二層拿到 handler function 的時候一併餵給 register() 註冊。iter_handlers 則是之後要根據 event type 依序取出註冊過的 event handler 會用到的。

至於最後面的 handler_managerhandle_event,是因為我實在是懶得寫這種程式還得特地實作一個 singleton(XD),所以直接在這裡宣告一個 global 層級的 manager。在大部分情況只需要用這個就夠了。

最後,main.py 的部分:

# file: main.py

from handler import handler_manager

modules = pkgutil.iter_modules(path=['./handlers'])  
for loader, mod_name, is_pkg in modules:  
    loader.find_module(mod_name).load_module(mod_name)


def trigger_event(event):  
    for handler in handler_manager.iter_handlers(event['type']):
        handler(event['text'])


if __name__ == '__main__':  
    trigger_event({'type': 'typeX', 'text': 'hello'})
    trigger_event({'type': 'typeY', 'text': 'world'})

Appendix:我就是要 Handler class

說到這裡,故事還沒結束。後來有個同事告訴我,他就是想把 handler 寫成 class。因為在處理 event 的過程中,需要呼叫每個 handler 的不同 method 來達成:

for handler in handlers:  
    # ...
    handler.do_first_thing(event)
    # do something else
    handler.do_second_thing(event)
    # ...

不得已(?)只好請回我們的 Handler class:

# file: handler.py

from abc import ABCMeta, abstractmethod


class Handler(object):

    __metaclass__ = ABCMeta

    @abstractmethod
    def do_first_thing(self, event):
        pass

    @abstractmethod
    def do_second_thing(self, event):
        pass

接著繼承並實作 handler:

# file: handlers/foo.py

from handler import Handler


@handle_event('typeA')
class MyHandler(Handler):

    def do_first_thing(self, event):
        print 'do first thing with event:' + event

    def do_second_thing(self, event):
        print 'do second thing with event: ' + event

注意到這裡我還是用了 @handle_event 來註冊 handler class。這是為了讓我們有能力令某些 Handler 的 subclass 不被註冊給 manager(只要註解掉 decorator,就可以取消註冊了)。

要用上面提到的 disabled property 來指定也是可以的,不過我個人比較偏好預設都不要主動註冊 handlers,而是由 decorator 來明確標示的方式。

最後,這裡的 HandlerManager 要略作修改:

# file: handler.py

# ...

class HandlerManager(object):

    # ...

    def register(self, handler_cls, *handled_events):
        self._handlers.append(_Handler(handler_cls(), set(handled_events)))

    # ...

因為原先是吃 function,現在是吃 class,因此在註冊時是註冊 class 的 instance 而不是 class 本身。(當然,要寫成註冊 class,然後 event 進來之後用它來建立 handler instance 也是種方法。基本上還是看哪種情況比較符合需求,這裡就不另外實作出來了。)

Conclusion

原本只是想整理的,結果好像被我越寫越亂了(XD)。

我想這個問題並不是非常難解。在一般的情況下,假如 handler 能實作成簡單的 function,我就不會想寫成 class。一些想在註冊時一併加入的資訊(如上面說的 handled_events)也能夠透過 decorator 傳入 register。

另外,雖然希望系統能自動 load 某個目錄下的所有 submodules,但我還是希望能明確標示是否要將 function/class 註冊(而不是預設自動註冊)為 event handler。對我來說,decorator 在這個問題上是我比較喜歡的解法。

  • LinkedIn
  • Tumblr
  • Reddit
  • Google+
  • Pinterest
  • Pocket
Comments powered by Disqus