ラクス Python Tips LT会 vol.2
2021/08/05 nikkie
ふだん デコレータ 使ってますか?
ガッツリ💪
たまに😃
初耳!👂
我が名はにっきー。アニメ大好き🥰この夏はアニメ映画がアツい!!
— nikkie 📣PyCon JP 2021 スタッフ募集中! (@ftnext) July 31, 2021
竜とそばかすの姫🐉、サイダーのように言葉が湧き上がる🥤、映画大好きポンポさん🎦、かくしごと👩🎨、きんモザ and so on!
しかしここ数日都内は感染者数3000人突破。外出しづらい。めっちゃ見たい!けどリスクとるか悩む。ぐおおお🤨
語弊を恐れずに言えば、 関数を返す関数
「関数が関数を返すってどういうこと?」🤨
👉 関数の基本から見ていきましょう
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)
で並べ替えた
クロージャ
関数は、ファーストクラスオブジェクト
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
体重の増減に興味(身長は大きく変わらない)
def bmi_calculator_with_height(height_m):
def calculate_bmi_wrapper(weight_kg):
return weight_kg / height_m / height_m
return calculate_bmi_wrapper
>>> 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の関数
デコレータ
より便利なデコレータを書くために
別の関数を返す関数で、通常、 @wrapper 構文で関数変換として適用されます
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_required
は post_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
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]
変数の参照のスコープ解決順
代入のスコープ解決
現在の関数のスコープ
(他の関数の中にある場合)外側のスコープ
グローバルスコープ(コードを含むモジュールのスコープ)
組み込みスコープ
『Effective Python 第2版』p.81
helper
の group
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
参照と代入それぞれのスコープの扱い
引数を取るデコレータ
@app.get("/")
async def root():
return {"message": "Hello World"}
3つの関数 で実装する
def decorator_with_args(x, y): # 引数を受け取りmiddleを返す
def middle(func): # 関数を受け取りwrapperを返す
def wrapper(*args, **kwargs):
...
return wrapper
return middle
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, **kwargs
・ functools.wraps
nonlocal
や引数を取るデコレータでより高機能に!
Enjoy development with decorators!
References、Appendix が続きます(よろしければどうぞ!)
『Effective Python 第2版』3章関数(特に以下)
項目21 クロージャが変数スコープとどう関わるかを把握しておく
項目22 可変長位置引数を使って、見た目をすっきりさせる
項目26 functools.wrapsを使って関数デコレータを定義する
The Global Dev Study #4 - FastAPI / Python
PyCon JP 2020 「詳解デコレータ」