基本部分
何番煎じか知らんけど、python で C# っぽいイベント機構を使うためのクラス。
ChatGPT に「Django の Signal を使うのもいいんじゃない?」とか言われたけど、なんか手軽さがないので自作。(てか、Python 標準ライブラリで用意されていてもいいレベルだと思うが、もっと pythonic なやり方があるんか?)
そして、なんか改善しようと頑張った人がいるっぽいが、私の欲しい機能としては以下で必要十分。てか、C# でも sender 渡すし。
"""A module for implementing event-driven programming in Python."""
from typing import Any
from collections.abc import Callable
class Event(Callable):
"""Event class"""
def __init__(self) -> None:
self._handlers = []
def __iadd__(self, other: Any) -> "Event":
if not isinstance(other, Callable):
raise TypeError(f"'{type(other)}' is not callable.")
if other in self._handlers:
raise ValueError(
f"Handler (object id: '{str(id(other))}') has already been added."
)
self._handlers.append(other)
return self
def __isub__(self, other: Any) -> "Event":
if other in self._handlers:
self._handlers.remove(other)
return self
def __call__(self, sender: object, *args, **kwargs) -> None:
for handler in self._handlers:
handler(sender, *args, **kwargs)
from . import event
class EventTestClass:
def __init__(self) -> None:
self._test_event = event.Event()
@property
def test_event(self) -> Event:
return self._test_event
@test_event.setter
def test_event(self, new_value) -> None:
if not new_value is self._test_event:
raise ValueError("New value must be the same as the current value.")
def test_event_happen(self) -> None:
self._test_event(self, msg="test event has happened.")
def test_handler(sender: object, msg: str) -> None:
print(msg)
if __name__ == '__main__':
obj = EventTestClass()
obj.test_event += my_handler
obj.test_event_happen()
test event has happened.
改善できたらいいなぁと思ってるところ
イベントごとにゲッター/セッターを書くのが面倒すぎる。デコレーターとかで何とかできないか…。案募集中。
案 1) デコレーターで何とかする
event_property というデコレーター (として使う関数) を追加した。
def event_property(fget):
"""A decorator for event properties.
This decorator can be applied to a method in a class that represents an event.
Note:
This decorator automatically generates an attribute with a name starting with "_"\
to store the Event object. The name is derived from the name of the decorated method.
"""
event_attr_name = "_" + fget.__name__
def getter(obj):
if not hasattr(obj, event_attr_name):
setattr(obj.__class__, event_attr_name, Event())
return getattr(obj, event_attr_name)
def setter(obj, value):
if not value is getattr(obj, event_attr_name):
raise ValueError("New value must be the same as the current value.")
return property(getter, setter)
from . import event
class EventTestClass:
@event.event_property
def test_event(self) -> Event:
pass
def test_event_happen(self) -> None:
self.test_event(self, msg="test event has happened.")
def test_handler(sender: object, msg: str) -> None:
print(msg)
if __name__ == '__main__':
obj = EventTestClass()
obj.test_event += my_handler
obj.test_event_happen()
あとは、test_event()
にあるpass
がなくせたら完璧だな (まぁ docstring 書いたら消せるw)。それと、プライベートなインスタンス フィールドをデコレーター内で名前決め打ちで作ってるけどいいのかな、みたいな?
いや、もう十分使いやすいけど。
案 2) __init__()
内で呼び出す関数でなんとかする
デコレーターで何とかする案は、結構複雑なうえに利用者が理解しづらそうなので、もっと単純な理屈のほうがよさげかなぁ。ということで作ってみた。
def add_event(obj: object, event_name: str) -> Event:
"""Add an event to an object.
This function is intended to be used inside the initializer of obj's class.
Args:
obj (object): The object to which the event is to be added.
event_name (str): The name of the event to be added.
Returns:
Event: The event object that was added to the object.
Note:
This function generates a property with a getter and a setter method, as well\
as an attribute with a name starting with "_" to store the Event object.
"""
event_obj = Event()
event_attr_name = "_" + event_name
setattr(obj, event_attr_name, event_obj)
def getter(obj):
return getattr(obj, event_attr_name)
def setter(obj, value):
if not value is getattr(obj, event_attr_name):
raise ValueError("New value must be the same as the current value.")
setattr(obj.__class__, event_name, property(getter, setter))
return event_obj
from . import event
class EventTestClass:
def __init__(self) -> None:
self.test_event = event.add_event(self, "test_event")
def test_event_happen(self) -> None:
self.test_event(self, msg="test event has happened.")
def test_handler(sender: object, msg: str) -> None:
print(msg)
if __name__ == '__main__':
obj = EventTestClass()
obj.test_event += my_handler
obj.test_event_happen()
うーん、解りやすくはなった気がする。が、test_event ってのが文字列リテラルで書かれてるせいで、プロパティ名とのズレが発生しそう。どうすりゃええんや…。
ていうのと、本当はself.test_event = event.add_event(self, "test_event")
じゃなく、event.add_event(self, "test_event")
だけでも動くんだけど、Pylint が文句言うんだよなぁ。冗長だけど、こうしないと…。
案 3) __setattr__()
をオーバーライドして何とかする
EventSource というイベントを発生する派生クラス群の基底クラスを定義し、__setattr__()
をオーバーライドすることで魔法をかける。
class EventSource:
"""Provides a mechanism for defining events in a class.
An event is a signal that something has happened, and any interested object can be notified of the event.
Note:
This implementation uses overriding the `__setattr__` method to detect the\
creation of event objects and turn them into properties with getters and\
setters that enforce the event contract.
"""
def __setattr__(self, name: str, value: Any) -> None:
if not isinstance(value, Event):
super().__setattr__(name, value)
return
event_attr_name = "_" + name
if (
hasattr(self, event_attr_name)
and not value is getattr(self, event_attr_name)
):
raise ValueError("New value must be the same as the current value.")
def getter(obj):
return getattr(obj, event_attr_name)
def setter(obj, new_value):
if not new_value is getattr(obj, event_attr_name):
raise ValueError("New value must be the same as the current value.")
setattr(self.__class__, name, property(getter, setter))
super().__setattr__(event_attr_name, value)
from . import event
class EventTestClass(event.EventSource):
def __init__(self) -> None:
self.test_event = event.Event()
def test_event_happen(self) -> None:
self.test_event(self, msg="test event has happened.")
def test_handler(sender: object, msg: str) -> None:
print(msg)
if __name__ == '__main__':
obj = EventTestClass()
obj.test_event += my_handler
obj.test_event_happen()
ちゃんと動くし、一見素直。ある程度問題は解決されてるんだけど、よく考えた時の「どう動いてるかわからん」感は、デコレーターの時よりひどい気がする。
どの方法がいいのでしょうか。
もしくは、もっといい方法があるのでしょうか。コメント待ってます。