Python で C# っぽいイベント機構を使うためのクラス

# 基本部分

何番煎じか知らんけど、python で C# っぽいイベント機構を使うためのクラス。

ChatGPT に「[Django の Signal](https://docs.djangoproject.com/en/4.1/topics/signals/) を使うのもいいんじゃない？」とか言われたけど、なんか手軽さがないので自作。(てか、Python 標準ライブラリで用意されていてもいいレベルだと思うが、もっと pythonic なやり方があるんか？)

そして、[なんか改善しようと頑張った人がいるっぽい](https://emptypage.jp/notes/pyevent.html)が、私の欲しい機能としては以下で必要十分。てか、C# でも sender 渡すし。

```python:event.py
"""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)
```

```python:使ってみる
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()
```

```text:出力
test event has happened.
```

# 改善できたらいいなぁと思ってるところ

イベントごとにゲッター/セッターを書くのが面倒すぎる。デコレーターとかで何とかできないか…。案募集中。

# 案 1) デコレーターで何とかする

event_property というデコレーター (として使う関数) を追加した。

```python:event.py (追加)
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)
```


```python:再度、使ってみる
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__()`内で呼び出す関数でなんとかする

デコレーターで何とかする案は、結構複雑なうえに利用者が理解しづらそうなので、もっと単純な理屈のほうがよさげかなぁ。ということで作ってみた。

```python:event.py (追加)
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
```

```python:再々度、使ってみる
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__()`をオーバーライドすることで魔法をかける。

```python: event.py (追加)
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)
```

```python:再々々度、使ってみる
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()
```

ちゃんと動くし、一見素直。ある程度問題は解決されてるんだけど、よく考えた時の「どう動いてるかわからん」感は、デコレーターの時よりひどい気がする。

# どの方法がいいのでしょうか。

もしくは、もっといい方法があるのでしょうか。コメント待ってます。