python – Pass self to decorator object, or Access self from decorator

個人的には、使おうとしたけど結局シックリこなくて破棄はした、けど思ったより悶着するネタなのでメモだけはしとく。こんなん必要になった時点では絶対忘れてると思うし。

まず、こんなコードを書いていたとする:

 1 from __future__ import unicode_literals
 2 
 3 import os, sys, logging
 4 
 5 def somefun(s, verbose):
 6     """
 7     do some.
 8     """
 9     curdir = os.path.abspath(os.curdir)
10     targetpath = os.path.abspath("../..")
11     if verbose:
12         logging.info("+ Entering '{}'.".format(targetpath))
13     try:
14         os.chdir(targetpath)
15         # do something
16         logging.info("perform " + s + "!!!")
17     finally:
18         if verbose and targetpath:
19             logging.info("- Leaving '{}'.".format(targetpath))
20         os.chdir(curdir)
21 
22 class DoSome(object):
23     def somemeth(self, s, verbose):
24         """
25         do some.
26         """
27         curdir = os.path.abspath(os.curdir)
28         targetpath = os.path.abspath("../..")
29         if verbose:
30             logging.info("+ Entering '{}'.".format(targetpath))
31         try:
32             os.chdir(targetpath)
33             # do something
34             logging.info("perform " + s + "!!!")
35         finally:
36             if verbose and targetpath:
37                 logging.info("- Leaving '{}'.".format(targetpath))
38             os.chdir(curdir)

モジュールレベルのものとクラスのものが両方自分のスクリプトに共存したわけではなくて、説明の都合両方あるとして。

スクリプト全体でこれがたった一箇所にしか現れないならワタシならこのままにしとく。問題はこれが2箇所以上で必要になった場合。こんな時こそコンテクストマネージャ、こんな時こそデコレータ、っしょ、てわけだ。

コンテクストマネージャの方は別になんてことない:

 1 # -*- coding: utf-8 -*-
 2 from __future__ import unicode_literals
 3 
 4 import os, sys, logging
 5 
 6 class ChangeDir(object):
 7     def __init__(self, path, verbose=False):
 8         self.targetpath = os.path.abspath(path).replace(os.sep, "/")
 9         self._verbose = verbose
10 
11     def __enter__(self):
12         self.curdir = os.path.abspath(os.curdir)
13         if self._verbose and self.targetpath:
14             logging.info("+ Entering '{}'.".format(self.targetpath))
15             os.chdir(self.targetpath)
16         return self
17 
18     def __exit__(self, type, value, tb):
19         if self._verbose and self.targetpath:
20             logging.info("- Leaving '{}'.".format(self.targetpath))
21         os.chdir(self.curdir)
22 
23 # 些細なこと: なんで logging の threshold 使わず info して verbose やねん、てのは
24 # 今やってるスクリプト固有の事情。threshold だけでは実現しにくい、ちょっと複雑な
25 # 「情報出力の冗長さ制御」が必要なの。

これを使えば:

 1 def somefun(s, verbose):
 2     """
 3     do some.
 4     """
 5     with ChangeDir("../..", verbose):
 6         # do something
 7         logging.info("perform " + s + "!!!")
 8 
 9 class DoSome(object):
10     def somemeth(self, s, verbose):
11         """
12         do some.
13         """
14         with ChangeDir("../..", verbose):
15             # do something
16             logging.info("perform " + s + "!!!")

まぁまぁスッキリはする。だけれども try~finally や with の唯一にして最大の問題はそう、「ネストが一段余計」なことだ。すなわち、ボディ部分が複雑になるほどソースコードの圧迫感が増していく。やはりデコレータを使いたいのだ。

最初のバージョンは素直に:

 1 from __future__ import unicode_literals
 2 
 3 import os, sys, logging
 4 from functools import wraps
 5 
 6 #class ChangeDir は省略
 7 
 8 def chdir0(path, verbose=False):
 9     def decorator(func):
10         @wraps(func)
11         def wrapper(*args, **kwargs):
12             with ChangeDir(path, verbose):
13                 return func(*args, **kwargs)
14         return wrapper
15     return decorator

functools.wraps は適用しないとドキュメンテーション文字列などがおかしなことになるので必要。

モジュールレベル関数への適用は問題ない:

1 @chdir0("../..", True)
2 def somefun(s, verbose):
3     """
4     do some!
5     """
6     # do something
7     logging.info("perform " + s + "!!!")

もちろんクラスメソッドへの適用も、以下は別に問題ない:

1 class DoSome(object):
2     @chdir0("../..", True)
3     def somemeth(self, s, verbose):
4         """
5         do some.
6         """
7         # do something
8         logging.info("perform " + s + "!!!")

もちろん「”../..” 固定」でいつでも済むはずはなく、ほとんどの場合これは動的に変化するものを渡したいのであって、典型的には何か設定ファイルに書き込まれたパスに移動したい、などであり、普通は self.rootdir などだろう。して問題のこれ:

 1 class DoSome(object):
 2     def __init__(self, root):
 3         self.root = root
 4 
 5     @chdir0(self.root, True)
 6     def somemeth(self, s, verbose):
 7         """
 8         do some.
 9         """
10         # do something
11         logging.info("perform " + s + "!!!")
12 
13 # ...
14 logging.basicConfig(stream=sys.stdout, level=logging.INFO)
15 d = DoSome("../..")
16 d.somemeth()

要はモジュールレベルで self が可視でないからなわけだけど、ともあれこうなる:

1 Traceback (most recent call last):
2   File "hoge.py", line 113, in <module>
3     class DoSome(object):
4   File "hoge.py", line 117, in DoSome
5     @chdir0(self.root, True)
6 NameError: name 'self' is not defined

やっと記事見出しのネタに辿りついた。

記事見出しの後者「Access self from decorator」は「here is nothing special you need to do to do this, just write an ordinary decorator」なのだ、はっきり言ってしまえば:

 1 def temp_chdir_root(verbose=False):
 2     def decorator(func):
 3         @wraps(func)
 4         def wrapper(self, *args, **kwargs):
 5             with ChangeDir(self.root, verbose):  # access to self
 6                 return func(self, *args, **kwargs)
 7         return wrapper
 8     return decorator
 9 
10 class DoSome(object):
11     def __init__(self, root):
12         self.root = root
13 
14     @temp_chdir_root(True)
15     def somemeth(self, s, verbose):
16         """
17         do some.
18         """
19         # do something
20         logging.info("perform " + s + "!!!")

「here is nothing special」とはいいつつ本当は「object specific」つまり適用する相手を熟知している場合にだけ使える。モジュール内共通、ということならこれでも事足りる場合もあると思う。

けれどもやっぱりさっきのダメだったこれ:

 1 class DoSome(object):
 2     def __init__(self, root):
 3         self.root = root
 4 
 5     @chdir0(self.root, True)  # NG!!
 6     def somemeth(self, s, verbose):
 7         """
 8         do some.
 9         """
10         # do something
11         logging.info("perform " + s + "!!!")

本当にやりたいのはこういうことなのだ。

結局 stackoverflow で紹介されていたアプローチを少しだけ変形したものがこれ:

 1 def chdir_meth(pathfunc, verbose=False):
 2     def decorator(func):
 3         @wraps(func)
 4         def wrapper(self, *args, **kwargs):
 5             with ChangeDir(pathfunc(self), verbose):
 6                 return func(self, *args, **kwargs)
 7         return wrapper
 8     return decorator
 9 
10 class DoSome(object):
11     def __init__(self, root):
12         self.root = root
13 
14     @chdir_meth(lambda self: self.root, True)
15     def somemeth(self, s, verbose):
16         """
17         do some.
18         """
19         # do something
20         logging.info("perform " + s + "!!!")

結局なんか嬉しくないなぁ、のはやはり、「モジュールレベル関数のものと共通化出来んもんなん?」てのと、もうひとつ、「lambda self: self.root なんてのがやっぱしイヤ」。

「モジュールレベル関数のものと共通化出来んもんなん?」については、イケなくはないんだけれど:

1 def chdir2(path, verbose=False):
2     def decorator(func):
3         @wraps(func)
4         def wrapper(*args, **kwargs):
5             _path = path if not callable(path) else path(args[0])
6             with ChangeDir(_path, verbose):
7                 return func(*args, **kwargs)
8         return wrapper
9     return decorator

つまり「lambda self: self.root はイヤでも受け容れ」た上で、callable かどうかを調べ、もしそうなら「args[0] が self であることをあてにして」呼び出す、てことをしている。つまりこれの適用はモジュールレベル関数になら普通に、メソッドなら「lambda self: self.root的」に、てこと:

 1 class DoSome(object):
 2     def __init__(self, root):
 3         self.root = root
 4 
 5     @chdir2(lambda self: self.root, True)
 6     def somemeth(self, s, verbose):
 7         """
 8         do some.
 9         """
10         # do something
11         logging.info("perform " + s + "!!!")
12 
13 rd = "../.."
14 
15 @chdir2(rd, True)
16 def somefun4(s, verbose):
17     """
18     do some!
19     """
20     # do something
21     logging.info("perform " + s + "!!!")

ね、シックリこないでしょ? それだけでなく、なくても別の手段でスッキリさせたので、結局これは今やってるヤツでは採用しなかった。けど必要となることもあると思うしね。

というわけだったのでした。おっしまい。