python でデコレータへの引数を増やしたくなったら…

たんまり5分は悩んでしまった。バカだ。

もともと関数への出入りをデバッグ用にロギングしたくて:

 1 import logging
 2 import inspect
 3 from functools import wraps
 4 
 5 def logging_enter_debug():
 6     def decorator(func):
 7         @wraps(func)
 8         def wrapper(*args, **kwargs):
 9             logger = logging.getLogger(
10                 inspect.getmodule(
11                     inspect.stack()[1][0]).__name__)
12             logger.debug("START '%s'", func.__name__)
13             try:
14                 return func(*args, **kwargs)
15             except Exception as e:
16                 logger.debug("! failed with <%s>" % e.__class__.__name__)
17                 raise
18             finally:
19                 logger.debug("END   '%s'", func.__name__)
20         return wrapper
21     return decorator

としてたとする。これによって、

1 class Hoge(object):
2     @logging_enter_debug()
3     def hoge(self, ...):
4         # ...

みたいに使えてハッピーだ、てわけだ。

けどこれ、「時々 debug じゃなく info にしたいかも/したくなったなぁ」時、コピペで二重化するのもやなので、引数で切り替えられるようにしようか、と。ただしアプリケーション全編で使いまくっているので、クライアントコードを壊すことなく。

無論愚直な答えは当たり前のもの:

 1 import logging
 2 import inspect
 3 from functools import wraps
 4 
 5 def logging_enter(level):
 6     def decorator(func):
 7         @wraps(func)
 8         def wrapper(*args, **kwargs):
 9             logger = logging.getLogger(
10                 inspect.getmodule(
11                     inspect.stack()[1][0]).__name__)
12             puts = logger.debug if level else logger.info
13             puts("START '%s'", func.__name__)
14             try:
15                 return func(*args, **kwargs)
16             except Exception as e:
17                 puts("! failed with <%s>" % e.__class__.__name__)
18                 raise
19             finally:
20                 puts("END   '%s'", func.__name__)
21         return wrapper
22     return decorator
23 
24 def logging_enter_info():
25     return logging_enter(0)
26 
27 def logging_enter_debug():
28     return logging_enter(1)

partial を使ってもうほんの少しだけスマートに:

 1 import logging
 2 import inspect
 3 from functools import wraps, partial
 4 
 5 def logging_enter(level):
 6     def decorator(func):
 7         @wraps(func)
 8         def wrapper(*args, **kwargs):
 9             logger = logging.getLogger(
10                 inspect.getmodule(
11                     inspect.stack()[1][0]).__name__)
12             puts = logger.debug if level else logger.info
13             puts("START '%s'", func.__name__)
14             try:
15                 return func(*args, **kwargs)
16             except Exception as e:
17                 puts("! failed with <%s>" % e.__class__.__name__)
18                 raise
19             finally:
20                 puts("END   '%s'", func.__name__)
21         return wrapper
22     return decorator
23 
24 
25 logging_enter_info = partial(logging_enter, level=0)
26 logging_enter_debug = partial(logging_enter, level=1)

これだけ。

ただその「5分の迷走」で最初書いちゃったのは:

 1 import logging
 2 import inspect
 3 from functools import wraps
 4 
 5 class _mk_logging_enter_deco():
 6     def __init__(self, level):
 7         self.level = level
 8 
 9     def __call__(self, func):
10         @wraps(func)
11         def wrapper(*args, **kwargs):
12             logger = logging.getLogger(
13                 inspect.getmodule(
14                     inspect.stack()[1][0]).__name__)
15             puts = logger.debug if self.level else logger.info
16             puts("START '%s'", func.__name__)
17             try:
18                 return func(*args, **kwargs)
19             except Exception as e:
20                 puts("! failed with <%s>" % e.__class__.__name__)
21                 raise
22             finally:
23                 puts("END   '%s'", func.__name__)
24         return wrapper
25 
26 
27 def logging_enter_info():
28     return _mk_logging_enter_deco(0)
29 
30 
31 def logging_enter_debug():
32     return _mk_logging_enter_deco(1)

無論これも解の一つではあるし問題なく期待通りのものだから「大馬鹿」ではないけれど、このためにあえて class にするのはツーマッチではあろう。

なんだかんだデコレータのソースって日常にしてないと迷子になりやすいんで、時折脳内でこんなことが起こったりするんだよね。(慣れていない人は、pdb なりや trace あるいは脳内でも紙面ででもマウスでもいいから、ステップ実行して追いかけてみるといい。)