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()
將 handler
與 handled_events
包成 namedtuple
加到 self._handlers
裡頭。handle_event()
跟剛剛的實作類似:第一層 function 的 parameters 會在第二層拿到 handler function 的時候一併餵給 register()
註冊。iter_handlers
則是之後要根據 event type 依序取出註冊過的 event handler 會用到的。
至於最後面的 handler_manager
與 handle_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 在這個問題上是我比較喜歡的解法。