@dataclass
のような、 ()
を付けても付けなくてもいいデコレータはどう作る?@dataclass
のような、 ()
を付けても付けなくてもいいデコレータはどう作る?Python駿河 勉強会 #28
2021/08/28 nikkie
お久しぶりです。実行委員会です。
— PyCon Mini Shizuoka (@PyconShizu) August 24, 2021
今年も PyCon mini Shizuoka 2021を開催することが決定しました🎉
開催日は 2021/11/20(土)です。
@dataclass
のような、 ()
を付けても付けなくてもいいデコレータを作りたい
@dataclass # ()を付けない
class C:
...
@dataclass() # ()を付ける
class C:
...
# デフォルトの値で各引数を指定
@dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)
class C:
...
@dataclass
のようなデコレータの要件@decorator
と @decorator()
が 同じ
その上で、 引数も渡せる
別の関数を返す関数で、通常、 @wrapper 構文で関数変換として適用されます( 用語集 より)
デコレータで関数呼び出しの前後に 追加コード を実行できる
今回 ()
を付けても付けなくてもいいデコレータを作りたかった
そのデコレータは 関数 をデコレートするのに使う
実装する上で、知っている範囲から @dataclass
を参考にした
@dataclass
の3つの例について、 だいたい等価 な書き方( 参考 )
@dataclass # C = dataclass(C)
class C:
...
@dataclass() # C = dataclass()(C)
class C:
...
# C = dataclass(init=True, ..., frozen=False)(C)
@dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)
class C:
...
@dataclass
はどう実現している?🔍CPythonの実装を見てみます(v3.9.6 Lib/dataclasses.py )
def dataclass(cls=None, /, *, init=True, repr=True, eq=True, order=False,
unsafe_hash=False, frozen=False):
def wrap(cls):
return _process_class(cls, init, repr, eq, order, unsafe_hash, frozen)
if cls is None:
return wrap
return wrap(cls)
Lib/dataclasses.py
の @dataclass
の実装 1/4
# 引数に **デフォルト値** が設定されている
def dataclass(cls=None, /, *, init=True, repr=True, eq=True, order=False,
unsafe_hash=False, frozen=False):
def wrap(cls):
return _process_class(cls, init, repr, eq, order, unsafe_hash, frozen)
if cls is None:
return wrap
return wrap(cls)
Lib/dataclasses.py
の @dataclass
の実装 2/4
def dataclass(cls=None, /, *, init=True, repr=True, eq=True, order=False,
unsafe_hash=False, frozen=False):
def wrap(cls):
# クロージャ:外側のスコープ(dataclass)の変数initなどを参照。
# 外側のスコープ(dataclass)の変数はデコレータの呼び出しで設定される
return _process_class(cls, init, repr, eq, order, unsafe_hash, frozen)
if cls is None:
return wrap
return wrap(cls)
Lib/dataclasses.py
の @dataclass
の実装 3/4
def dataclass(cls=None, /, *, init=True, repr=True, eq=True, order=False,
unsafe_hash=False, frozen=False):
def wrap(cls):
return _process_class(cls, init, repr, eq, order, unsafe_hash, frozen)
# @dataclass() や @dataclass(frozen=True) では cls is None
if cls is None:
# C = dataclass()(C) 👉 C = wrap(C)
return wrap
return wrap(cls)
Lib/dataclasses.py
の @dataclass
の実装 4/4
def dataclass(cls=None, /, *, init=True, repr=True, eq=True, order=False,
unsafe_hash=False, frozen=False):
def wrap(cls):
return _process_class(cls, init, repr, eq, order, unsafe_hash, frozen)
if cls is None:
return wrap
# @dataclass では cls は、デコレータに続いて定義されるクラスを指す
# C = dataclass(C) 👉 C = wrap(C) となっている
return wrap(cls)
@dataclass
を参考に実装したデコレータ @using_firefox
@using_firefox
@logged_in
def show_copy_popup(url):
copy_existing_event(url, human_confirms=True)
ブラウザ操作 ライブラリ(2600 star)
Seleniumのラッパーで、 非常に簡単 に書ける!💫
詳しくは 9/11(土) #pycharity のLTで共有します
@using_firefox
ブラウザ(Firefox)を起動する処理 helium.start_firefox
は必ず呼ぶ
デコレータで実装することで、 関数呼び出しの前に必ず追加 できる!🤩
さらに @using_chrome
とデコレータを変えたら起動するブラウザも変わる実装🆒
@using_firefox(options=...)
helium.start_firefox
に selenium.webdriver.FirefoxOptions
を渡したい
例:ダウンロードのポップアップを出さないようにFirefoxを設定する
1つのデコレータ @using_firefox
で実現したく今回取り組んだ
@using_firefox(options=...)
の例
@using_firefox(options=options)
@logged_in
def download(url):
download_latest_participants_csv(url)
@dataclass
にならって実装
def using_firefox(func=None, /, *, options=None):
def middle(func):
@wraps(func)
def wrapper(*args, **kwargs):
start_firefox(options=options) # 関数呼び出し前に追加したかった!
func(*args, **kwargs)
return wrapper
if func is None:
return middle
return middle(func)
@using_firefox()
でデコレートしたとき( ()
を付けて)
def using_firefox(func=None, /, *, options=None):
# middle の定義は省略
if func is None:
# f = using_firefox()(f) 👉 f = middle(f)
return middle
return middle(func)
@using_firefox
でデコレートしたとき( ()
を付けないで)
def using_firefox(func=None, /, *, options=None):
# middle の定義は省略
if func is None:
return middle
# f = using_firefox(f) 👉 f = middle(f)
return middle(func)
@dataclass
のような、 ()
を付けても付けなくてもいいデコレータはどう作る?()
を付けても付けなくてもいいデコレータの作り方第1引数 func
/ cls
の デフォルト値 を None
にする
func
/ cls
の値で 分岐
()
を付けても付けなくてもいいデコレータの作り方(承前)func
/ cls
の値で分岐
None
なら、クラス/関数を引数に受け取る 関数 を返す( ()
付きに対応)
None
でないなら、クラス/関数を引数に受け取る関数に func
/ cls
を渡した 返り値 を返す( ()
なしに対応)
実装には クロージャ を利用
この関数の中で、 デコレータの引数を参照 して実装する
クロージャ: functions that refer to variables from the scope in which they were defined (『Effective Python second edition』Item 21 p.84)
Enjoy development with decorators!
References、 Appendix が続きます(よろしければどうぞ!)
位置専用・キーワード専用引数
@dataclass
のようなデコレータの別の例
print
を仕込んで decorator()(f)
呼び出し順を確認
def using_firefox(func=None, /, *, options=None):
func
は 位置専用 引数(🙅♂️ using_firefox(func=f)
)
options
は キーワード専用 。 options=...
と指定する必要がある(位置引数として指定できない 🙅♂️ using_firefox(func, options)
)
位置専用 の場合、引数の順序が重要であり、キーワードで引数を渡せません。 位置専用引数は
/
(スラッシュ)の前に配置されます。
https://docs.python.org/ja/3/tutorial/controlflow.html#positional-only-parameters
引数をキーワード引数で渡す必要があることを示す キーワード専用 として引数をマークするには、引数リストの最初の キーワード専用 引数の直前に
*
を配置します。
https://docs.python.org/ja/3/tutorial/controlflow.html#keyword-only-arguments
@dataclass
のようなデコレータの別の例
def once_per_minutes(func=None, /, *, n=1):
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
if func is None:
return middle
return middle(func)
引数を取るデコレータの例:呼び出しはN分に1回(詳しくはReferencesのスライドをどうぞ)
今回の内容をもとに、 引数を取らない使い方もできる ように拡張した
実行環境 Python 3.9.4
>>> calculate_bmi1(1.58, 46)
18.426534209261334
>>> # calculate_bmi1(1.58, 46) # Raise RunTooOftenError
>>> calculate_bmi2(1.58, 46)
18.426534209261334
>>> # calculate_bmi2(1.58, 46) # Raise RunTooOftenError
>>> calculate_bmi3(1.58, 46)
18.426534209261334
>>> calculate_bmi3(1.58, 46)
18.426534209261334
print
を仕込んで decorator()(f)
呼び出し順を確認
def decorator(func=None, /, *, n=1):
print(f"decorator start: {func=} {n=}")
def middle(func):
print("middle start")
def wrapper(*args, **kwargs):
print(f"wrapper start: {args=}, {kwargs=}")
func(*args, **kwargs)
print("wrapper end")
print("middle end")
return wrapper
print("decorator end")
if func is None:
return middle
return middle(func)
()
なしでデコレート
>>> @decorator
... def f1(): ...
...
decorator start: func=<function f1 at 0x1092a9a60> n=1 # funcにf1が渡っている
decorator end
middle start # middle(func) を返したことによる実行
middle end
()
を付け、デフォルト値でデコレート
>>> @decorator()
... def f2(): ...
...
decorator start: func=None n=1
decorator end
middle start # f2 = middle(f2) 部分
middle end
middle
が返っている 1/2
>>> decorator()
decorator start: func=None n=1
decorator end
<function decorator.<locals>.middle at 0x1092ba280> # decorator() の返り値
middle
が返っている 2/2
>>> def f(): ...
...
>>> decorator()(f)
decorator start: func=None n=1
decorator end
middle start
middle end
<function decorator.<locals>.middle.<locals>.wrapper at 0x1092ba430>
>>> f2 # decorator()(f) と同様に wrapper
<function decorator.<locals>.middle.<locals>.wrapper at 0x1092ba550>
()
を付け、値も指定してデコレート
>>> @decorator(n=3)
... def f3(): ...
...
decorator start: func=None n=3 # nは渡した値
decorator end
middle start
middle end
print
で確認した呼び出し順(1)~(3)の3例とも 出力されるメッセージは同様
つまり ()
の有無によらず、関数がデコレートされている
分岐の実装により ()
の有無によらず、関数がデコレートされる