デコレータについて

Event

ラクス Python Tips LT会 vol.2

Presented

2021/08/05 nikkie

❓ Question(チャットお願いします🙏)

ふだん デコレータ 使ってますか?

  • ガッツリ💪

  • たまに😃

  • 初耳!👂

お前、誰よ

  • Python大好き にっきー @ftnext / @ftnext

  • Python歴3年半。データサイエンティストにしてNLPer

  • Py thon Con ference JP 2021 座長🇨🇭

  • tips: Python界隈では 自己紹介のエイリアス が「お前、誰よ」(ルーツ

お前、誰よ(承前)

今回のテーマは「デコレータ」

デコレータ

  • 語弊を恐れずに言えば、 関数を返す関数

  • 「関数が関数を返すってどういうこと?」🤨

  • 👉 関数の基本から見ていきましょう

お品書き:デコレータについて

  • Pythonの関数

  • デコレータ

  • より便利なデコレータを書くために

お品書き:デコレータについて

  • Pythonの関数

  • デコレータ

  • より便利なデコレータを書くために

Pythonの関数


def calculate_bmi(height_m, weight_kg):
    # ref: https://en.wikipedia.org/wiki/Body_mass_index
    return weight_kg / height_m / height_m

>>> calculate_bmi(1.58, 46)
18.426534209261334

@skip (寄り道) 関数のtips

  • 返り値として複数の式を返せる(タプル)

  • yield によるジェネレータ( vol.1のスライド 参照)

  • calculate_bmi(height_m=1.58, weight_kg=46) とも呼び出せる

    • Python 3.8~ 位置のみ・キーワードのみ引数

優先ソート


>>> values = [1, 4, 3, 5, 2]
>>> # valuesを素数を優先して並び替え(素数の昇順、残りの昇順)
>>> sort_priority(values, {2, 3, 5})
>>> values
[2, 3, 5, 1, 4]
>>> # valuesを偶数を優先して並び替え(素数の昇順、残りの昇順)
>>> sort_priority(values, {2, 4})
>>> values
[2, 4, 1, 3, 5]

優先ソート実装:関数の中に関数


def sort_priority(values, group):
    def helper(x):
        if x in group:
            return (0, x)
        return (1, x)
    values.sort(key=helper)

https://github.com/bslatkin/effectivepython/blob/master/example_code/item_21.py#L50

sort_priority(values, {2, 3, 5})

  • values = [1, 4, 3, 5, 2]

  • helper の返り値

    • group については (0, 2), (0, 3), (0, 5)

    • group にないもの (1, 1), (1, 4)

  • key: (1, 1), (1, 4), (0, 3), (0, 5), (0, 2) で並べ替えた

優先ソートの例から2点

  • クロージャ

  • 関数は、ファーストクラスオブジェクト

(再掲)優先ソート実装


def sort_priority(values, group):
    def helper(x):  # クロージャ
        if x in group:
            return (0, x)
        return (1, x)
    values.sort(key=helper)  # ファーストクラスオブジェクト

クロージャ

  • functions that refer to variables from the scope in which they were defined (『Effective Python second edition』Item 21 p.84)

  • =自身が定義されたスコープから(外側のスコープの)変数を参照する関数(訳の案)

  • 優先ソートの helper 関数

    • sort_priority の引数 group参照 している

ファーストクラスオブジェクト

  • 関数を 「直接参照でき、変数に代入したり、他の関数の引数として渡したり、式の中やif文の中で比較」 できる(『Effective Python 第2版』項目21 p.80)

  • 優先ソートの values.sort(key=helper)

    • sort メソッドの key 引数に関数を渡し ている

関数を返す関数

  • 関数がファーストクラスオブジェクトであることとクロージャを利用する

  • 例:特定の人のBMI

  • 体重の増減に興味(身長は大きく変わらない)

例:ある身長の人のBMIを算出する関数を返す


def bmi_calculator_with_height(height_m):
    def calculate_bmi_wrapper(weight_kg):
        return weight_kg / height_m / height_m
    return calculate_bmi_wrapper

ある身長の人のBMIの変化を求める


>>> calculate_her_bmi = bmi_calculator_with_height(1.58)
>>> calculate_her_bmi(46)
18.426534209261334
>>> calculate_her_bmi(42)
16.824226886716872
>>> calculate_her_bmi(50)
20.0288415318058

🥟小まとめ:Pythonの関数

  • 関数の中に関数を書く例を見てきた

  • クロージャ:内側の関数から外側の関数の引数を 参照 できる

  • ファーストクラスオブジェクト:内側の関数を外側の関数の 返り値 にできる

お品書き:デコレータについて

  • Pythonの関数

  • デコレータ

  • より便利なデコレータを書くために

デコレータ(用語集)

別の関数を返す関数で、通常、 @wrapper 構文で関数変換として適用されます

https://docs.python.org/ja/3/glossary.html#term-decorator

デコレータの例(Django)


from django.contrib.auth.decorators import login_required

@login_required
def post_new(request):  # デコレータをつけることで、ログイン必須にできる
    ...

https://tutorial-extensions.djangogirls.org/ja/authentication_authorization#nono

デコレータ

  • 「ラップする関数への呼び出しの前後で追加コードを実行することができます」 (『Effective Python 第2版』項目26 p.97)

  • Djangoの login_requiredpost_new 関数の前に、ログインを検証するコードを実行した

@ はシンタックスシュガー

以下の2つは同じ


def f(...):
    ...

f = awesome_decorator(f)

@awesome_decorator
def f(...):
    ...

簡単なデコレータを作ってみる

  • ラップする関数に 追加 するコード

    • 実行前に開始する旨を出力

    • 実行後に終了する旨を出力

関数の開始と終了を示すデコレータ


def show_start_end(func):
    def wrapper(*args, **kwargs):
        print(func.__name__, "Start")
        returned = func(*args, **kwargs)
        print(func.__name__, "End")
        return returned
    return wrapper

関数の開始と終了を示すデコレータを使う


@show_start_end
def calculate_bmi(height_m, weight_kg):
    return weight_kg / height_m / height_m

>>> calculate_bmi(1.58, 46)
calculate_bmi Start
calculate_bmi End
18.426534209261334

*args ? **kwargs ?

2つのシーンで使う

  • 関数定義

  • 関数呼び出し

関数定義の *args**kwargs

  • *args:可変個の 位置 引数

    • 関数のスコープでは argsタプル

  • **kwargs:可変個の キーワード 引数

    • 関数のスコープでは kwargs辞書

例:関数定義の *args**kwargs


def f(*args, **kwargs):
    print(args)
    print(kwargs)

>>> f(1, 2, c=3)
(1, 2)
{'c': 3}

例:関数定義の *args**kwargs 実行例


>>> # 位置引数のみ渡した時、kwargsは空の辞書
>>> f(1, 2, 3)
(1, 2, 3)
{}
>>> # キーワード引数のみ渡した時、argsは空のタプル
>>> f(a=1, b=2, c=3)
()
{'a': 1, 'b': 2, 'c': 3}

関数呼び出しの *args**kwargs

  • タプルやリストなど シーケンス* 演算子で、要素を位置引数に渡せる

  • 辞書など マッピング** 演算子で、キー=値 形式で各要素をキーワード引数に渡せる

例:関数呼び出しの *args**kwargs 実行例


>>> # シーケンスを * を使って渡す(argsがタプルとして受け取る)
>>> f(*[1,2,3])
(1, 2, 3)
{}
>>> # マッピングを ** を使って渡す(kwargsが辞書として受け取る)
>>> f(**{"a": 1, "b": 2, "c": 3})
()
{'a': 1, 'b': 2, 'c': 3}

再掲:関数の開始と終了を示すデコレータ


def show_start_end(func):
    def wrapper(*args, **kwargs):
        print(func.__name__, "Start")
        # wrapper に渡された位置引数、キーワード引数はfuncに渡されるということ
        returned = func(*args, **kwargs)
        print(func.__name__, "End")
        return returned
    return wrapper

デコレータを作るときのtips

  • f = awesome_decorator(f) という代入の落とし穴

落とし穴😱


@show_start_end
def fully_documented_bmi(height_m, weight_kg):
    """Calculate BMI (body mass index)."""
    return weight_kg / height_m / height_m

>>> fully_documented_bmi.__name__  # show_start_endが返す関数の名
'wrapper'
>>> print(fully_documented_bmi.__doc__)  # 失われたdocstring
None

💡解決策: functools.wraps


from functools import wraps

def show_start_end(func):
    @wraps(func)  # wrapperをデコレートする
    def wrapper(*args, **kwargs):
        print(func.__name__, "Start")
        returned = func(*args, **kwargs)
        print(func.__name__, "End")
        return returned
    return wrapper

functools.wraps で落とし穴回避🙌


@show_start_end
def fully_documented_bmi(height_m, weight_kg):
    """Calculate BMI (body mass index)."""
    return weight_kg / height_m / height_m

>>> fully_documented_bmi.__name__  # デコレータを付けた関数自体の名!
'fully_documented_bmi'
>>> print(fully_documented_bmi.__doc__)  # docstringが失われていない!
Calculate BMI (body mass index).

🥟小まとめ:デコレータ

  • 関数の呼び出し前後に処理を追加できる

  • 関数を返す関数 として実装

  • 関数定義と関数呼び出しで *args**kwargs を使う

  • ワンポイント: functools.wraps

@skip (時間があれば)私が車輪の再実装したデコレータ


def cache_enabled(func):
    cache = {}

    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        key = kwargs["number"]
        if key in cache:
            return cache[key]
        retval = func(*args, **kwargs)
        cache[key] = retval
        return retval

    return wrapper

お品書き:デコレータについて

  • Pythonの関数

  • デコレータ

  • より便利なデコレータを書くために

より便利なデコレータを書くために

  • 参照と代入それぞれのスコープの扱い

  • 引数を取るデコレータ

例:優先したかどうかを返す優先ソート


def sort_priority(values, group):
    found = False
    def helper(x):
        if x in group:
            found = True
            return (0, x)
        return (1, x)
    values.sort(key=helper)
    return found

https://github.com/bslatkin/effectivepython/blob/master/example_code/item_21.py#L65

優先したかを返していない?🤨


>>> values = [1, 4, 3, 5, 2]
>>> # valuesを素数を優先して並び替えているが、返り値はFalse
>>> sort_priority(values, {2, 3, 5})
False
>>> values
[2, 3, 5, 1, 4]

なぜ優先したかを返さない?

  • 変数の参照のスコープ解決順

  • 代入のスコープ解決

変数の参照のスコープ解決順

  1. 現在の関数のスコープ

  2. (他の関数の中にある場合)外側のスコープ

  3. グローバルスコープ(コードを含むモジュールのスコープ)

  4. 組み込みスコープ

『Effective Python 第2版』p.81

例:クロージャ helpergroup


def sort_priority(values, group):
    found = False
    def helper(x):
        if x in group:  # 外側のスコープのgroupを参照
            found = True
            return (0, x)
        return (1, x)
    values.sort(key=helper)
    return found

代入のスコープ解決

変数が現在のスコープに存在しないと、Pythonは、代入を変数定義のように扱います

『Effective Python 第2版』p.81

なぜ優先したかを返さなかったか?


def sort_priority(values, group):
    found = False
    def helper(x):
        if x in group:
            # 以下はhelperのスコープで変数foundを定義
            found = True  # 2行目のfoundとは無関係
            return (0, x)
        return (1, x)
    values.sort(key=helper)
    return found  # sort_priorityのスコープのfound

💡解決策: nonlocal


def sort_priority(values, group):
    found = False
    def helper(x):
        nonlocal found  # sort_priorityのfound
        if x in group:
            found = True  # helperのスコープから代入できる!(nonlocalの効果)
            return (0, x)
        return (1, x)
    values.sort(key=helper)
    return found

優先したかを返すように!🙌


>>> values = [1, 4, 3, 5, 2]
>>> sort_priority(values, {2, 3, 5})
True
>>> values
[2, 3, 5, 1, 4]

nonlocal を使ったデコレータ:呼び出しは1分に1回


def once_per_minute(func):
    last_ran_at = 0

    @wraps(func)
    def wrapper(*args, **kwargs):
        nonlocal last_ran_at
        current_time = time.time()
        if current_time - last_ran_at < 60:
            raise RunTooOftenError(
                f"Wait longer before running {func.__name__}"
            )
        last_ran_at = current_time
        value = func(*args, **kwargs)
        return value

    return wrapper

より便利なデコレータを書くために

  • 参照と代入それぞれのスコープの扱い

  • 引数を取るデコレータ

デコレータの例(FastAPI)


@app.get("/")
async def root():
    return {"message": "Hello World"}

https://fastapi.tiangolo.com/ja/tutorial/first-steps/

引数を取るデコレータはどう書く?

  • 3つの関数 で実装する


def decorator_with_args(x, y):  # 引数を受け取りmiddleを返す
    def middle(func):  # 関数を受け取りwrapperを返す
        def wrapper(*args, **kwargs):
            ...
        return wrapper
    return middle

引数を取るデコレータ:呼び出しはN分に1回


def once_per_n_minutes(n):
    def middle(func):
        last_ran_at = 0

        @wraps(func)
        def wrapper(*args, **kwargs):
            nonlocal last_ran_at
            current_time = time.time()
            if current_time - last_ran_at < n * 60:
                raise RunTooOftenError(
                    f"Wait longer before running {func.__name__}"
                )
            last_ran_at = current_time
            value = func(*args, **kwargs)
            return value

        return wrapper

    return middle

引数を渡してデコレート


@once_per_n_minutes(0)
def calculate_bmi2(height_m, weight_kg):
    return calculate_bmi(height_m, weight_kg)

@once_per_n_minutes(n=3)
def calculate_bmi3(height_m, weight_kg):
    return calculate_bmi(height_m, weight_kg)

引数を渡してデコレート 実行例


>>> calculate_bmi2(1.58, 46)
18.426534209261334
>>> calculate_bmi2(1.58, 46)
18.426534209261334
>>> calculate_bmi3(1.58, 46)
18.426534209261334
>>> # calculate_bmi3(1.58, 46)  # Raise RunTooOftenError

🥟小まとめ:より便利なデコレータを書くために

  • 外側のスコープの変数、参照できるが、 代入 には nonlocal

  • 引数を取るデコレータは、 3つ の関数で実装

🌯まとめ:デコレータについて

  • デコレータ=関数を返す関数。 処理を追加 できる

  • 実装の鍵は、クロージャ・ *args, **kwargsfunctools.wraps

  • nonlocal や引数を取るデコレータでより高機能に!

ご清聴ありがとうございました

Enjoy development with decorators!

References、Appendix が続きます(よろしければどうぞ!)

References 1/2

  • Effective Python 第2版』3章関数(特に以下)

    • 項目21 クロージャが変数スコープとどう関わるかを把握しておく

    • 項目22 可変長位置引数を使って、見た目をすっきりさせる

    • 項目26 functools.wrapsを使って関数デコレータを定義する

References 2/2

Appendix 作成中

EOF