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__関数を定義する
- ドットによるオブジェクトの属性の取得をカスタマイズするための__getattr__と__getattribute__
- 公式ドキュメント
Details
class Neko:
def nyan(self):
print("nyaan")
def meow(self):
print("meow")
def default_response(self):
print ("wagahai am neko")
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
nyaan
meow
以上のようなNeko
クラスがあったとき,このNeko
がなく瞬間に別のオブジェクトが反応してほしい.
いま猫がどこ出身かを診断する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
に代入)する.
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')
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
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
を変更すると,
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になる
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()
! 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()
では
- 前置callback
- retval = neko.nyan()
- 後置callback
- return retval
の順で実行されている.
したがって後置Callbackそれ自体,あるいはCallbackによってフラグが立って再開された別スレッド上の手続きなどが
返されたretvalを受け取る左辺値を監視している場合,実行時その左辺値が変更されていない状態で動いてしまうことになる.
例えば,今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があるミュータブル変数を監視してる
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
が監視しているバッファに格納しようとする.
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では元メソッドの返り値を含むリストが返ってきてほしいのに
そうではない状態になっている.
! 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
ステイトメントの中で代入操作をすることでこれを解決してみる.
+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
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は
! 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
どちらかといえば犬派です.