Decoratorの分類 A-E
このセッションでは、PythonにおけるDecoratorの使い方について、発表者がケース別にA-Eで分類したものについて発表しました。
Decoratorは汎用性が高く、応用範囲も広いためさまざまな場面で活用できます。
このセッションでは経験に基づく実装が多いDecoratorの数々を、ケース別に体系的に分類しており、とてもわかりやすかったため紹介したいと思います。
A - Argument Changing Decorators
Argument Changing Decoratorsは関数呼び出し時に、引数の追加や削除、変更をするDecoratorです。また、引数の変更だけでなく返り値を変更するパターンもこれに該当します。
このパターンでは、共通して使用する値を明示的に注入するためにDecoratorとして付与したり、1つの関数で複数ケースの値を与えるような時に用いられます。
例えば、関数呼び出し時に引数を追加するDecoratorは以下のようなものです。
関数実行時に関数名を渡すDecoratorの例
def fn_with_name(func):
def wrapper(*args, **kwargs):
func(*args, func.__name__, **kwargs)
return wrapper
@fn_with_name
def do_something(arg1, func_name):
print('{name}: {arg1}'.format(name=func_name, arg1=arg1))
do_something('here') # do_something: here
1つの関数で複数ケースの値を渡すDecoratorの例(pytestの@pytest.mark.parametrize)
import pytest
from datetime import datetime, timedelta
testdata = [
(datetime(2001, 12, 12), datetime(2001, 12, 11), timedelta(1)),
(datetime(2001, 12, 11), datetime(2001, 12, 12), timedelta(-1)),
]
@pytest.mark.parametrize("a,b,expected", testdata)
def test_timedistance_v0(a, b, expected):
diff = a - b
assert diff == expected
B - Binding Decorators
Binding Decoratorsは関数の振る舞いがどのようなものかをDescriptor[7]を用いて定義したDecoratorです。
普段よく目にする標準ライブラリの@staticmethod、@classmethod、@property等のDecoratorもこれに該当します。
インスタンスメソッドであることを明示するDecoratorの例
class instance_method:
def __init__(self, func):
self.func = func
def __get__(self, inst, cls):
if inst is None:
raise TypeError(f'{self.func.__name__} is only valid on instances.')
class GoodClass:
@instance_method
def simple_method(self):
print("simple_method")
def normal_method(self):
print("normal_method")
GoodClass().simple_method() # simple_method
GoodClass().normal_method() # normal_method
GoodClass.normal_method # <function __main__.GoodClass.normal_method>
GoodClass.simple_method # TypeError: simple_method is only valid on instances.
C - Control Flow Decorators
Control Flow Decoratorsは関数が呼ばれるかどうか、または、何回呼び出されるかを制御するためのDecoratorです。
よくあるパターンとしては、あるAPI呼び出し関数の発火条件として認証済みであることが必須の場合に、認証済みであれば実行し、認証済みでなければ関数を実行せずエラーを返したり、Exceptionを送出するパターンがあります。また、APIのリトライなどを明示する場合にも用いられます。
成功するまでリトライするDecoratorの例
def infinite_retry(func):
def wrapper(*args, **kwargs):
while True:
try:
return func(*args, **kwargs)
except RuntimeError as e:
print(e)
@infinite_retry
def random_fail(max_value):
ret = random.randint(-100, max_value)
if ret < 0:
raise RuntimeError("Invalid negative number {ret}.format(ret=ret)")
return ret
random_fail(10)
# Invalid negative number -79
# Invalid negative number -67
# 6
D - Descriptive Decorators
Descriptive Decoratorsは装飾されたオブジェクト(関数の戻り値など)をコレクションに追加するDecoratorです。
これにより作成されたコレクションはドキュメント化やディスパッチング、プラグインなどを提供するために使用されます。
少し難しい言い回しかもしれませんが、FlaskのルーティングDecoratorなどがこれに該当します。
FlaskのルーティングではDecoratorの引数として与えられたパスに対して対象の関数を追加する操作をしています[8]。
FlaskにおけるルーティングDecoratorの例
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello World!'
E - Execution Decorators
Execution DecoratorsはDecoratorに与えられたコードを解釈して関数を実行するDecoratorです。
Python以外のコードを再解釈して実行する際に広く利用されます。
実行時にはDecoratorによって変換・生成されたコードが呼び出されるため、動作を追うのが難しく注意が必要です。しかし、使い方によってはPythonへ任意のコードを橋渡しすることが可能になります。
コード置換するDecoratorの例
def replacer(old, new):
def wrapper(func):
source = inspect.getsource(func)
lines = source.split('\n')
new_source = lines[1] + '\n' + ('\n'.join(lines[2:]).replace(old, new))
exec(new_source, globals())
return globals()[func.__name__]
return wrapper
@replacer('b', 'b*3')
def calc(a, b):
x = a + b
y = x * 2
print('Result: ' + str(y))
calc(1, 4) # 26
Cythonの型へ割り当てを行うDecoratorの例
cython = MagicMock()
@cython.locals(a=cython.double, b=cython.doule, n=cython.p_double)
def foo(a, b, x, y):
n = a * b
...
以上がセッションで発表されたDecoratorの5分類です。
セッション動画の最後では、既存のライブラリで使用されているDecoratorが、どれに分類されるかについてconclusionとしてまとめていますので、興味がある方は動画のリンクからご覧ください。
DecoratorはPythonの表現を豊かにしている言語仕様の1つだと思います。なので、既存のユースケースやセッションで紹介された分類を頭の中で整理しておくことで、よりPythonらしいコードを書けるようになると思います。
今後Decoratorを見かけた時には、どの分類のDecoratorなのかを考えてみるのも楽しいかもしれませんね。
注
[7] Descriptorとは振る舞いを束縛したオブジェクト属性のことを指し、__get__、__set__、__delete__メソッドのいずれかをオーバーロードしたオブジェクトを意味します。
[8] flask.app.routeの実装(Github)
おわりに
第2回にわたってEuroPython 2018のレポートをお届けしました。
第1回EuroPython 2018レポートでは、会場の雰囲気や基調講演、Python3.7の新機能を中心に紹介し、第2回では筆者らが興味を持ったセッションを中心に紹介してきました。
EuroPythonはその規模もさることながら、セッション数がとても多く、かつ、並行して進むため筆者らが聴けなかったセッションもあります。そのため今回紹介しきれなかったセッションも数多くあり、紹介できたセッションは全体のごく一部と言えます。
EuroPython 2018の公式チャンネルで動画が公開されているので、興味のある方は是非ご覧ください。
今回カンファレンスに参加された日本の方は筆者ら含め合計4人でしたが、カンファレンスを通してその方たちや現地の方などとの出会いもあり、とても有意義な時間を過ごせました。
EuroPython 2019も開催が決定しているので、機会があればまた是非参加したいです。そのときには、本記事をご覧いただいた皆さんとお会いできることも楽しみにしています!
最後までご覧いただき、ありがとうございました。
