Pythonの__getattr__()を使ってObserverデザインパターンを実装

# Abstruct
PythonでPush型の非同期的デザインであるObserverパターン（Subscribe-Publish）を`__getattr__()`を使って実装してみた．
**（改良を追記しました．Githubのコードはこの記事の範囲の最終版になります）**
https://github.com/Philosophistoria/Python_ObserverDesign

PythonでObserberパターンについては以下の記事でも：
https://qiita.com/ttsubo/items/34690c8e9351f5b2e989
https://qiita.com/Algos/items/ba9c6ed52380b57aecac

ただ`__getattr__`を用いた実装はなかったので今回記事を書いてみた．
もしかしたらあまりよくない方法かも？
まあたぶんオーバーヘッドは大きいです．

が，もともとPythonで速さを求めてないし，実装が簡単だと思います．


# Background
PythonでCallbackを実現するのは結構容易いが，
既存のクラスにCallbackを登録するような変更を加えるのがちょっと嫌だったのと，
すべてのメソッドに対して通知が欲しいが，メソッドが多すぎて面倒な場合，
これらの変更ができない場合（かなり面倒くさい場合）があった．

そこでObservableにしたいオブジェクトをラップすることでその実現ができないかと模索したところ
`__getattr__`を使うことでそれが可能っぽかったので，実験してみた．

`__getattr__`の説明は以下参考：
- [【Python中級者への道】クラスに__getattr__関数を定義する](https://qiita.com/ganariya/items/4f80b10d1d362e6c0fab)
- [ドットによるオブジェクトの属性の取得をカスタマイズするための__getattr__と__getattribute__](https://qiita.com/fumitoh/items/b090ae6ccd52e6a88d9e)
- [公式ドキュメント](https://docs.python.org/ja/3/reference/datamodel.html#object.__getattr__)

# Details

```python:Neko

class Neko:
    def nyan(self):
        print("nyaan")

    def meow(self):
        print("meow")

    def default_response(self):
        print ("wagahai am neko")
```

```python:input
neko = Neko()
neko.nyan()
neko.meow()
# neko.neee()           # <-- AttributeError: 'Neko' object has no attribute 'neee'
# neko.nyan("nekoneko") # <-- TypeError: nyan() takes 1 positional argument but 2 were given
```

```python:output
nyaan
meow
```

以上のような`Neko`クラスがあったとき，この`Neko`がなく瞬間に別のオブジェクトが反応してほしい．
いま猫がどこ出身かを診断する`NekoObserver`がいたとする．

```python:NekoObserver
class NekoObserver:
    def listen(self, nakigoe):
        if (nakigoe == "meow"):
            print("this nekosama meows!")
        else:
            print("this nekosama is from Japan")
```

この`NekoObserver`の`listen`メソッドを`Neko`オブジェクトである`neko`にアタッチし
`neko`が鳴くたびにその猫がどこ出身かを判別してほしい．

しかし，今のところ`Neko`クラスにはそういった機能は載っていない．
そこで以下のようなクラスを用意しラップ（引数`notifier`に代入）する．


```python:ObservableWrapper
class HeroicNotifier:
    def __init__(self, notifier, callback=None):
        self.callbacks = [] 
        self.notifier = notifier
        self.attatch(callback)

    def attatch(self, callback):
        if (callable(callback)):
            self.callbacks.append(callback)

    def __getattr__(self, name):
        for callback in self.callbacks:
            callback(name)
        if hasattr(self.notifier, name):
            return getattr(self.notifier, name)
        elif hasattr(self.notifier, "default_response"):
            return getattr(self.notifier, "default_response")
        else:
            return self.__default_action
            # same as:
            # return getattr(self, "__default_action")

    def __default_action(self, *args):
        print ('no such attribute')
```



```python:input
neko = Neko()                                # <--- neko 用意
nekodignosis = NekoObserver()                # <--- 猫出身地診断Observer用意
heroic_neko = HeroicNotifier(neko)           # <--- Observableなnekoになっていただく
heroic_neko.attatch(nekodignosis.listen)     # <--- nekoの鳴き声に耳を傾ける

# neko が鳴いてみる
heroic_neko.nyan()
heroic_neko.meow()
heroic_neko.neee()
# heroic_neko.nyan("nyaaaan") # <-- TypeError: nyan() takes 2 positional argument but 2 were given
```

```:output
this nekosama is from Japan
nyaan
this nekosama meows!
meow
this nekosama is from Japan
wagahai am neko
```
となって，猫が鳴く直前にステレオタイプに基づいて猫を日本出身認定している．~~きもい．~~

今のところCallbackを実行してから，もとのメソッドを実行しかできない．

これはある猫メソッド`heroic_neko.nyan()`がよばれたとき，
まず`()`の直前の`heroic_neko.nyan`までの読み込みにおいて`heroic_neko.__attribute__()`がよばれ，
`heroic_neko`が`nyan`というattributeを持っていない場合`heroic_neko.__getattr__()`が呼ばれ，
でこの中でCallbackを実行してから，もとの`neko.nyan()`の参照`neko.nyan`を返し，
最後に`neko.nyan()`を実行しているためである．

すなわち，`{heroic_neko.nayn --> neko.nyan}()`という形になっている．伝わってほしい．

どうにか`return`したあとに少し処理を続けられるようにするか，
左辺値の関数ポインタを変更するかなどできればできそうかもだけど，
たぶんPythonだともっといい方法があるのかな．

これ書いた後にちょっと調べたら以下のスレッドに似たようなコードがあった（気がする）
https://stackoverflow.com/questions/13528213/observer-observable-classes-in-python

# P.S.
元のメソッドを読んだ後にCallbackを呼ぶのは元のメソッドをラップしたメソッドの参照（の値）を返せばできた．
すなわち，以下のように`ObsarvableWrapper`を変更すると，

```diff:ObservableWrapper
class HeroicNotifier:
    def __init__(self, notifier, callback=None):
        self.callbacks = [] 
        self.notifier = notifier
        self.attatch(callback)

    def attatch(self, callback):
        if (callable(callback)):
            self.callbacks.append(callback)

    def __getattr__(self, name):
-       for callback in self.callbacks:
-           callback(name)
-       if hasattr(self.notifier, name):
-           return getattr(self.notifier, name)
-       elif hasattr(self.notifier, "default_response"):
-           return getattr(self.notifier, "default_response")
-       else:
-           return self.__default_action
-           # same as:
-           # return getattr(self, "__default_action")
+       self.attr_name
+       return __wrapper
       
    def __default_action(self, *args):
        print ('no such attribute')
+
+   def __wrapper(self, *args):
+       if hasattr(self.notifier, self.attr_name):
+           self.callable_attr = getattr(self.notifier, self.attr_name)
+       elif hasattr(self.notifier, "default_response"):
+           self.callable_attr = getattr(self.notifier, "default_response")
+       else:
+           self.callable_attr = self.__default_action
+           # same as:
+           # getattr(self, "__default_action")
+
+       retval = None
+
+       for callback in self.callbacks:    #<--- pre method callback
+           callback(self.attr_name)
+
+       retval = self.callable_attr(*args)
+
+       for callback in self.callbacks:    #<--- post method callback
+           callback(self.attr_name)
+
+       return retval
```

以下のようなInputとOutputになる

```python:input
    print("\n! Ordinal neko:\n")
    neko = Neko()
    neko.nyan()
    neko.meow()
    # neko.neee()           # <-- AttributeError: 'Neko' object has no attribute 'neee'
    # neko.nyan("nekoneko") # <-- TypeError: nyan() takes 1 positional argument but 2 were given

    print("\n! noko dignosis:\n")
    nekodignosis = NekoObserver()
    heroic_neko = HeroicNotifier(neko)
    heroic_neko.attatch(nekodignosis.listen)
    heroic_neko.nyan()
    heroic_neko.meow()

    print("\n! neko dont have neee voice:\n")
    heroic_neko.neee()
```

```text:output
! Ordinal neko:

nyaan
meow

! noko dignosis:

this nekosama is from Japan
nyaan
this nekosama is from Japan
this nekosama meows!
meow
this nekosama meows!

! neko dont have neee voice:

this nekosama is from Japan
wagahai am neko
this nekosama is from Japan
```

`neko`が鳴いたあとにも出身地診断をしている．

今`heroic_neko.nyan()`では

1. 前置callback
2. retval = neko.nyan()
3. 後置callback
4. return retval

の順で実行されている．

したがって後置Callbackそれ自体，あるいはCallbackによってフラグが立って再開された別スレッド上の手続きなどが
返されたretvalを受け取る左辺値を監視している場合，実行時その左辺値が変更されていない状態で動いてしまうことになる．

例えば，今`Neko`クラスが以下のようなとき

```diff:Neko

    def nyan(self):
-       print("nyaan")
+       return "nyaan"

    def meow(self):
-       print("meow")
+       return "meow"

    def default_response(self):
-       print("wagahai am neko")
+       return "wagahai am neko"
```
でObserverがあるミュータブル変数を監視してる

```diff:NekoObserver
class NekoObserver:
+   def __init__(self, buf=[None]):
+       self.buf = buf

    def listen(self, nakigoe):
        if (nakigoe == "meow"):
            print("this nekosama meows!")
+           print(self.buf)
        else:
            print("this nekosama is from Japan")
+           print(self.buf)
```

そしてメイン手続きの中で`neko`の各メソッドからの返り値（各鳴き声）
を`NekoObserver`が監視しているバッファに格納しようとする．

```diff:input
if __name__ == "__main__":
    print("\n! Ordinal neko:\n")
    neko = Neko()
-   neko.nyan()
+   print(neko.nyan())
-   neko.meow()
+   print(neko.meow())

    print("\n! noko dignosis:\n")
+   retval = [None]   #<--- NekoObserverの監視するミュータブルオブジェクト
    nekodignosis = NekoObserver(retval) #<--- 監視させる
    heroic_neko = HeroicNotifier(neko)
    heroic_neko.attatch(nekodignosis.listen)
-   heroic_neko.nyan()
+   retval[0] = heroic_neko.nyan()      #<--- 返り値をリストに入れる
-   heroic_neko.meow()
+   retval[0] = heroic_neko.meow()      #<--- 返り値をリストに入れる

    print("\n! neko dont have neee voice:\n")
-   heroic_neko.neee()
+   retval[0] = heroic_neko.neee()      #<--- 返り値をリストに入れる
```
これに対する`output`は以下のようになり，後置Callbackでは元メソッドの返り値を含むリストが返ってきてほしいのに
そうではない状態になっている．

```text:output
! Ordinal neko:

nyaan
meow

! noko dignosis:

this nekosama is from Japan
[None]
this nekosama is from Japan
[None]                       #<--- ここが'nyaan'になってほしい
this nekosama meows!
['nyaan']
this nekosama meows!
['nyaan']                    #<--- ここが'meow'になってほしい

! neko dont have neee voice:

this nekosama is from Japan
['meow']
this nekosama is from Japan
['meow']                     #<--- ここが'wagahai am neko'になってほしい
```

#P.S.2
そこで`constlib.constmaneger`を使ったラッピングで`with`ステイトメントの中で代入操作をすることでこれを解決してみる．

```diff:ObservableWrapper
+import contextlib
 class HeroicNotifier:
     def __init__(self, notifier, callback=None):
         self.callbacks = [] 
         self.notifier = notifier
         self.attatch(callback)
 
     def attatch(self, callback):
         if (callable(callback)):
             self.callbacks.append(callback)
 
     def __getattr__(self, name):
         self.attr_name = name 
         return self.__wrapper

     def __default_action(self, *args):
         print ('no such attribute')
+
+    @contextlib.contextmanager
     def __wrapper(self, *args):
         if hasattr(self.notifier, self.attr_name):
             self.callable_attr = getattr(self.notifier, self.attr_name)
         elif hasattr(self.notifier, "default_response"):
             self.callable_attr = getattr(self.notifier, "default_response")
         else:
             self.callable_attr = self.__default_action
             # same as:
             # getattr(self, "__default_action")
-
-        retval = None
        
         for callback in self.callbacks:
             callback(self.attr_name)
-
-        retval = self.callable_attr(*args)
+
+        yield self.callable_attr(*args)   #<--- ここでいったんwith stamentのas以降に返り値を格納

         for callback in self.callbacks:
             callback(self.attr_name)
-
-        return retval
```

```diff:input
 if __name__ == "__main__":
     print("\n! Ordinal neko:\n")
     neko = Neko()
     print(neko.nyan())
     print(neko.meow())

     print("\n! noko dignosis:\n")
     retval = [None]
     nekodignosis = NekoObserver(retval)
     heroic_neko = HeroicNotifier(neko)
     heroic_neko.attatch(nekodignosis.listen)
-    retval[0] = heroic_neko.nyan()
+    with heroic_neko.nyan() as whatnekosays:
+        retval[0] = whatnekosays
-    retval[0] = heroic_neko.meow()
+    with heroic_neko.meow() as whatnekosays:
+        retval[0] = whatnekosays

     print("\n! neko dont have neee voice:\n")
-    retval[0] = heroic_neko.neee()
+    with heroic_neko.neee() as whatnekosays:
+        retval[0] = whatnekosays
```

とするとoutputは

```text:output
! Ordinal neko:

nyaan
meow

! noko dignosis:

this nekosama is from Japan
[None]
this nekosama is from Japan
['nyaan']                   #<--- 後置Callbackでちゃんと所望の鳴き声が反映されている
this nekosama meows!
['nyaan']
this nekosama meows!
['meow']               　    #<--- 後置Callbackでちゃんと所望の鳴き声が反映されている

! neko dont have neee voice:

this nekosama is from Japan
['meow']
this nekosama is from Japan
['wagahai am neko']          #<--- 後置Callbackでちゃんと所望の鳴き声が反映されている
```

# Conclusion
どちらかといえば犬派です．