【Python】体当たりで学ぶデコレータとスコープ
こんにちは。DI部の春田です。
Pythonのデコレータを使いこなすには、スコープの理解が必要です。スコープの概念を理解しても、いざ実際に実装しようとすると正しく扱えていなかったりすることがあるかと思います。
今回は概念的の話は後回しにし、1つのサンプルコードに対してトライ&エラーを繰り返して、Pythonのデコレータとスコープを体当たりで理解していきたいと思います。概念の話は検索すればたくさんヒットすると思うので、そちらを当たっていただけたらと思います。
今回使用するサンプルコードです。(Python 3.7.3)
var_a
~var_e
: 5つの変数var_f
~var_h
: 3つの引数(パラメータ)outer(var_f)
:inner(var_g)
を内包する関数inner(var_g)
: メインの関数decorator(var_h)
:inner(var_g)
をラッピングするデコレータ
var_a = "var_a" print('--- L3: Define decorator()') def decorator(var_h): var_c = "var_c" print('--- L6: In decorator() -> VAR_X = {}'.format(VAR_X)) def _decorator(f): var_d = "var_d" print('--- L10: In _decorator() -> VAR_X = {}'.format(VAR_X)) def wrapper(var_g): print("--- L13: Before decorate") print('--- L14: In wrapper() -> VAR_X = {}'.format(VAR_X)) var_e = "var_e" f(var_g) print("--- L17: After decorate") return wrapper return _decorator print('--- L22: Define outer()') def outer(var_f): print("--- L24: Into outer()") var_b = "var_b" print('--- L26: In outer() -> VAR_X = {}'.format(VAR_X)) @decorator("var_h") def inner(var_g): # innerからどの変数を参照・変更できるかを調べます print("--- L31: Into inner()") print('[Ref] L32: inner() -> VAR_X = {}'.format(VAR_X)) VAR_X = "CHANGED" print('[Chg] L34: inner() -> VAR_X = {}'.format(VAR_X)) print('--- L36: Execute inner()') inner("var_g") print('--- L38: In outer() -> VAR_X = {}'.format(VAR_X)) print('--- L41: Execute outer()') outer("var_f") print('--- Finish')
各変数がどこのスコープで参照できるのか調べるために、変数 VAR_X
を var_a
から var_h
に置換して検証していきます。結果として、指定した変数が inner()
の中で参照・変更できるよう調節を繰り返します。 inner()
では、指定された変数を参照し、変更して、再度参照するという処理を行なっています。GitHubにサンプルコード置いておきますので、よかったらご自身でも試してみてください。
GitHub - TakumiHaruta/decorator_practice: Easy examples for decorators and scopes
一つずつ var_a
から検証していきましょう。
目次
var_a
var_a
はプログラムの一番最初に定義されるグローバル変数です。 VAR_X
を var_a
に書き換え実行すると、以下のエラーが吐き出されます。
# エラー --- L3: Define decorator() --- L22: Define outer() --- L41: Execute outer() --- L24: Into outer() --- L26: In outer() -> var_a = var_a --- L6: In decorator() -> var_a = var_a --- L10: In _decorator() -> var_a = var_a --- L36: Execute inner() --- L13: Before decorate --- L14: In wrapper() -> var_a = var_a --- L31: Into inner() Traceback (most recent call last): File "a_deco.py", line 42, in <module> outer("var_f") File "a_deco.py", line 37, in outer inner("var_g") File "a_deco.py", line 16, in wrapper f(var_g) File "a_deco.py", line 32, in inner print('[Ref] L32: inner() -> var_a = {}'.format(var_a)) UnboundLocalError: local variable 'var_a' referenced before assignment
順をたどると、まずL3 decorator()
とL22 outer()
が定義され、L41で outer()
が実行されます。L26の通り、 outer()
の中からはグローバル変数の var_a
を参照できているようですね。その後 inner()
を定義する前に decorator()
の中に入っていきます。 decorator()
と _decorator()
内でも var_a
を参照することができています。そしてL36で inner()
の実行に移り、 wrapper()
の中が実行され、 inner()
の中に入っていきます。 inner()
内で var_a
を参照する際に UnboundLocalError
を吐きました。
UnboundLocalError
によると「ローカル変数の var_a
が代入される前に参照されている」とのことですが、これは変数名が同じであっても、ローカル( inner()
)の var_a
とグローバルの var_a
では扱いが違うことを示しています。 inner()
はまず、ローカル内の var_a
を参照しようとし、存在しなければ外のスコープの var_a
を参照しに行こうとします。このため、たとえL32で「グローバル変数の var_a
を呼び」、L33で「ローカルの var_a
に CHANGED
を代入する」挙動を意図したとしても、参照する順番が違うため人間の感覚通りには動いてくれません。
inner()
で var_a
を参照する方法は、大きく3通りあります。
#1. グローバル変数の var_a
を参照するだけの場合
→ L33の var_a = "CHANGED"
をコメントアウトする
inner()
内にローカル変数の var_a
を定義しないことで、 inner()
は外のスコープの var_a
を探しに行くようになります。
# 結果1 --- L3: Define decorator() --- L22: Define outer() --- L41: Execute outer() --- L24: Into outer() --- L26: In outer() -> var_a = var_a --- L6: In decorator() -> var_a = var_a --- L10: In _decorator() -> var_a = var_a --- L36: Execute inner() --- L13: Before decorate --- L14: In wrapper() -> var_a = var_a --- L31: Into inner() [Ref] L32: inner() -> var_a = var_a [Chg] L34: inner() -> var_a = var_a --- L17: After decorate --- L38: In outer() -> var_a = var_a --- Finish
#2. グローバル変数の var_a
参照し、ローカル変数として変更する場合
→ L29 def inner(var_g)
の引数を var_a
に変え、L37で inner(var_g)
に与えているパラメータ
"var_g"
を var_a
に変更する
inner()
のパラメータにグローバル変数の var_a
を渡してあげることで、 inner()
内で var_a
を参照することができるようになります。またこの時、 inner()
内で値を CHANGED
に変更した var_a
は、 outer()
で参照するときには値が var_a
に戻っています。これは、 CHANGED
を代入した var_a
はローカル変数であり、グローバル変数の var_a
は影響を受けていないということですね。
# 結果2 --- L3: Define decorator() --- L22: Define outer() --- L41: Execute outer() --- L24: Into outer() --- L26: In outer() -> var_a = var_a --- L6: In decorator() -> var_a = var_a --- L10: In _decorator() -> var_a = var_a --- L36: Execute inner() --- L13: Before decorate --- L14: In wrapper() -> var_a = var_a --- L31: Into inner() [Ref] L32: inner() -> var_a = var_a [Chg] L34: inner() -> var_a = CHANGED --- L17: After decorate --- L38: In outer() -> var_a = var_a --- Finish
#3. グローバル変数の var_a
を参照・変更する場合
→ L32で参照する前に global var_a
を宣言しておく
var_a
はグローバル変数であることを宣言しておけば、inner()
からでも var_a
を参照でき、さらにグローバル変数として変更を加えることができます。
# 結果3 --- L3: Define decorator() --- L22: Define outer() --- L41: Execute outer() --- L24: Into outer() --- L26: In outer() -> var_a = var_a --- L6: In decorator() -> var_a = var_a --- L10: In _decorator() -> var_a = var_a --- L36: Execute inner() --- L13: Before decorate --- L14: In wrapper() -> var_a = var_a --- L31: Into inner() [Ref] L32: inner() -> var_a = var_a [Chg] L34: inner() -> var_a = CHANGED --- L17: After decorate --- L38: In outer() -> var_a = CHANGED --- Finish
var_b
続いて、 outer()
内のローカル変数 var_b
に対して検証していきます。
## エラー1 --- L3: Define decorator() --- L22: Define outer() --- L41: Execute outer() --- L24: Into outer() --- L26: In outer() -> var_b = var_b Traceback (most recent call last): File "b_deco.py", line 42, in <module> outer("var_h") File "b_deco.py", line 28, in outer @decorator("var_g") File "b_deco.py", line 6, in decorator print('--- L6: In decorator() -> var_b = {}'.format(var_b)) NameError: name 'var_b' is not defined
グローバル変数の var_a
とは異なり、 var_b
は outer()
内の関数からでしか参照ができないため、デコレータからは直接参照することができません。デコレータの中で var_b
を参照するには、var_b
を decorator()
のパラメータに渡すか、inner()
のパラメータに渡すかの2通りの方法がありますが、今回はデコレータ内の参照をコメントアウトして再実行します。
## エラー2 --- L3: Define decorator() --- L22: Define outer() --- L41: Execute outer() --- L24: Into outer() --- L26: In outer() -> var_b = var_b --- L36: Execute inner() --- L13: Before decorate --- L31: Into inner() Traceback (most recent call last): File "b_deco.py", line 42, in <module> outer("var_f") File "b_deco.py", line 37, in outer inner("var_g") File "b_deco.py", line 16, in wrapper f(var_g) File "b_deco.py", line 32, in inner print('[Ref] L32: inner() -> var_b = {}'.format(var_b)) UnboundLocalError: local variable 'var_b' referenced before assignment
inner()
の中に入り、L32で UnboundLocalError
を吐きました。 var_b
の参照方法は、 var_a
の場合とほぼ同様で、 var_a
の #1 #2 は全く同じです。すなわち、 L33 var_b = "CHANGED"
をコメントアウトするか、 inner()
にパラメータとして渡すかという方法になります。そして var_b
での #3 の方法は global
ではなく、 nonlocal
を宣言することで参照・変更できるようになります。
nonlocal var_b
の結果は以下の通りです。 nonlocal
宣言をすることで、L38で outer()
のスコープで呼んでいる var_b
も、inner()
内での変更が反映されています。
## 結果 --- L3: Define decorator() --- L22: Define outer() --- L41: Execute outer() --- L24: Into outer() --- L26: In outer() -> var_b = var_b --- L36: Execute inner() --- L13: Before decorate --- L31: Into inner() [Ref] L32: inner() -> var_b = var_b [Chg] L34: inner() -> var_b = CHANGED --- L17: After decorate --- L38: In outer() -> var_b = CHANGED --- Finish
var_c & var_d & var_e
デコレータの中で生成されている変数 var_c
var_d
var_e
はそれぞれネストが異なりますが、 inner()
から参照するだけの今回の実験では、それほど大差はありません。代表例として var_c
を見ていきましょう。
まず、 outer()
と decorator()
は別のスコープであるため、 outer()
からは var_c
は呼ぶことができません。L26とL38はコメントアウトして実行します。
## エラー --- L3: Define decorator() --- L22: Define outer() --- L41: Execute outer() --- L24: Into outer() --- L6: In decorator() -> var_c = var_c --- L10: In _decorator() -> var_c = var_c --- L36: Execute inner() --- L13: Before decorate --- L14: In wrapper() -> var_c = var_c --- L31: Into inner() Traceback (most recent call last): File "c_deco.py", line 42, in <module> outer("var_f") File "c_deco.py", line 37, in outer inner("var_g") File "c_deco.py", line 16, in wrapper f(var_g) File "c_deco.py", line 32, in inner print('[Ref] L32: inner() -> var_c = {}'.format(var_c)) UnboundLocalError: local variable 'var_c' referenced before assignment
UnboundLocalError
を吐きました。ただし今回のケースでは、 inner()
から var_c
を直接参照することができないため、 var_a
のように var_c = "CHANGED"
をコメントアウトしても解決できません。 var_c
を参照するためには、デコレータ内の変数を inner()
に渡す #2 のような方法しかありません。L16を f(var_c)
、 L29を def inner(var_c)
に変更した結果は以下の通りです。
# 結果 --- L3: Define decorator() --- L22: Define outer() --- L41: Execute outer() --- L24: Into outer() --- L6: In decorator() -> var_c = var_c --- L10: In _decorator() -> var_c = var_c --- L36: Execute inner() --- L13: Before decorate --- L14: In wrapper() -> var_c = var_c --- L31: Into inner() [Ref] L32: inner() -> var_c = var_c [Chg] L34: inner() -> var_c = var_c --- L17: After decorate --- Finish
var_c
の実験で注目してほしい点は、 ラッピングされた関数からは、ラッピングしているデコレータ内のオブジェクトを直接参照することができない という点です。decorator()
内の各階層、 _dacorator()
や wrapper()
では var_c
を直接参照できていますが、 decorator()
とスコープが異なる関数 outer()
や inner()
からでは、 decorator
内のローカル変数を直接参照することができません(逆も然り)。「ラッピング(包む)」というワードを聞くと「デコレータで関数をネストしているのか!」となって、あたかもデコレータ内の変数も参照できると勘違いしてしまいますよね(僕だけでしょうか)。スコープに厳密になると、デコレータを表現する単語としては、"wrapper" よりかは "background" の方がしっくり来る気がします。
var_f
今度は、「変数」ではなく「引数とパラメータ」で定義された値を参照していきます。
var_f
は outer()
の引数とパラメータです。 decorator()
内からは参照することができないので、L6 10 14 はコメントアウトします。結果は以下の通りです。
# エラー --- L3: Define decorator() --- L22: Define outer() --- L41: Execute outer() --- L24: Into outer() --- L26: In outer() -> var_f = var_f --- L36: Execute inner() --- L13: Before decorate --- L31: Into inner() Traceback (most recent call last): File "f_deco.py", line 42, in <module> outer("var_f") File "f_deco.py", line 37, in outer inner("var_g") File "f_deco.py", line 16, in wrapper f(var_g) File "f_deco.py", line 32, in inner print('[Ref] L32: inner() -> var_f = {}'.format(var_f)) UnboundLocalError: local variable 'var_f' referenced before assignment
outer()
内の変数 var_b
とほぼ同じ状況です。 #1 #2 の方法でも参照できますし、#3 のように nonlocal var_f
で宣言することもできます。 nonlocal
は、「引数とパラメータ」として与えられたオブジェクトに対しても使用することができます。
var_g
var_g
は inner()
の引数とパラメータです。 inner()
を呼ぶ際に作成される変数なので、 outer()
から呼ぶことはできませんし、decorator()
内でも参照することはできません。L6 10 14 26 38をコメントアウトして実行します。
# 結果 --- L3: Define decorator() --- L22: Define outer() --- L41: Execute outer() --- L24: Into outer() --- L36: Execute inner() --- L13: Before decorate --- L31: Into inner() [Ref] L32: inner() -> var_g = var_g [Chg] L34: inner() -> var_g = CHANGED --- L17: After decorate --- Finish
var_g
は、何のエラーもなく終了しました。
var_h
最後は、 decoeator()
の引数とパラメータ var_h
です。 decorator()
内から参照できないので、L6 10 14をコメントアウトして実行します。
# エラー --- L3: Define decorator() --- L22: Define outer() --- L41: Execute outer() --- L24: Into outer() --- L6: In decorator() -> var_h = var_h --- L10: In _decorator() -> var_h = var_h --- L36: Execute inner() --- L13: Before decorate --- L14: In wrapper() -> var_h = var_h --- L31: Into inner() Traceback (most recent call last): File "h_deco.py", line 42, in <module> outer("var_f") File "h_deco.py", line 37, in outer inner("var_g") File "h_deco.py", line 16, in wrapper f(var_g) File "h_deco.py", line 32, in inner print('[Ref] L32: inner() -> var_h = {}'.format(var_h)) UnboundLocalError: local variable 'var_h' referenced before assignment
このケースも var_c
の時とほぼ一緒ですね。 wrapper()
内で var_h
を inner()
の引数に渡してあげると解決します。
# 結果 --- L3: Define decorator() --- L22: Define outer() --- L41: Execute outer() --- L24: Into outer() --- L6: In decorator() -> var_h = var_h --- L10: In _decorator() -> var_h = var_h --- L36: Execute inner() --- L13: Before decorate --- L14: In wrapper() -> var_h = var_h --- L31: Into inner() [Ref] L32: inner() -> var_h = var_h [Chg] L34: inner() -> var_h = CHANGED --- L17: After decorate --- Finish
さて、この引数を すり替え たり 細工 したりできることが、デコレータの特徴の一つです。デコレータは、ラッピングされる関数側で指定された引数(ここでは var_g
)と同じ引数に対して処理することができます。しかし先ほども述べましたが、 wrapper()
内で使用されているオブジェクトは、引数で渡さない限り wrapper()
の外から参照できません。その点に注意しましょう。
複雑なデコレータの実験
おまけですが、 decorator()
と inner()
に同じ引数 var_i
を与える実験をしてみたいと思います。それぞれの var_i
が、どこで置き換わるのかちょっと興味湧いてきませんか?
以下のサンプルコードを実行してみます。 _decorator()
から外のスコープ deocrator()
の変数に変更を加えることはできないので、事前にL8で nonlocal var_i
を宣言しています。
print('--- L1: Define decorator()') def decorator(var_i): print('--- L3: In decorator() -> var_i = {}'.format(var_i)) var_i = "decorator" print('--- L5: In decorator() -> var_i = {}'.format(var_i)) def _decorator(f): nonlocal var_i print('--- L9: In _decorator() -> var_i = {}'.format(var_i)) var_i = "_decorator" print('--- L11: In _decorator() -> var_i = {}'.format(var_i)) def wrapper(var_i): print("--- L14: Before decorate") print('--- L15: In wrapper() -> var_i = {}'.format(var_i)) var_i = "wrapper" f(var_i) print("--- L18: After decorate") return wrapper return _decorator print('--- L23: Define outer()') def outer(): print("--- L25: Into outer()") var_i = "var_i" print('--- L27: In outer() -> var_i = {}'.format(var_i)) @decorator(var_i) def inner(var_i): # innerからどの変数を参照・変更できるかを調べます print("--- L32: Into inner()") print('[Ref] L33: inner() -> var_i = {}'.format(var_i)) var_i = "CHANGED" print('[Chg] L35: inner() -> var_i = {}'.format(var_i)) print('--- L37: Execute inner()') inner(var_i) print('--- L39: In outer() -> var_i = {}'.format(var_i)) print('--- L42: Execute outer()') outer() print('--- Finish')
結果は以下の通りです。
## 結果 --- L1: Define decorator() --- L23: Define outer() --- L42: Execute outer() --- L25: Into outer() --- L27: In outer() -> var_i = var_i --- L3: In decorator() -> var_i = var_i --- L5: In decorator() -> var_i = decorator --- L9: In _decorator() -> var_i = decorator --- L11: In _decorator() -> var_i = _decorator --- L37: Execute inner() --- L14: Before decorate --- L15: In wrapper() -> var_i = var_i --- L32: Into inner() [Ref] L33: inner() -> var_i = wrapper [Chg] L35: inner() -> var_i = CHANGED --- L18: After decorate --- L39: In outer() -> var_i = var_i --- Finish
デコレータに渡された var_i
はL28で inner()
を定義する時に _decorator()
まで実行され、 inner()
を実行する際には inner()
で呼ばれた var_i
に置き換わっていることがわかります。いやぁーまったく、デコレータは複雑ですね。(笑)
では、今回の検証からわかったことをまとめていきたいと思います。
気づいたこと・まとめ
デコレータを定義する時は、スコープの関係性が重要
デコレータは関数のスコープを意識しなければ、上手に実装できません。デコレータ内で扱うオブジェクトと、デコレートされる関数内で扱うオブジェクト間に、依存関係がないように気をつけながら、様々な関数に対して使い回せる汎用的な実装を心がける必要があります。
デコレータはデコレートされる関数の引数を加工できる
デコレータが力を発揮する場面の一つは、関数の引数に対して共通の処理を行う時だと考えられます。その点、flaskのデコレータの使い方はかなり参考になりますね。
View Decorators — Flask 0.12.4 documentation
デコレートされる関数が定義される前に、 wrapper()
の外の処理が先に実行されている
var_i
の例でも見られるように、 wrapper()
外の処理は、デコレートされる関数が 定義される時 に先に実行され、 wrapper()
内の処理はデコレートされる関数が 呼び出された時 に実行されます。この順序の違いを意識して、デコレータの処理を wrapper()
の外に書くか、中に書くか決める必要があります。
global
や nonlocal
は、宣言した変数をグローバル変数・ノンローカル変数にするものではない
これは個人的に勘違いしていたのですが、 global
宣言や nonlocal
宣言の意味は、宣言した変数に対してスコープ外からも参照・変更を許可することではありません。これらの宣言でやっていることは、 スコープ外にある変数を、スコープ内にある同名のローカル変数と紐付ける ことであり、グローバル変数として存在しない変数に対して global
宣言をしても、グローバル変数としては扱うことはできません。
引数として与えられたオブジェクトに対しても nonlocal
宣言することができる
これが可能なことによって、少しわかりづらいスコープをもつ変数が定義される可能性があります。あまり見ない例かもしれませんが...
最後に
長々とお読みいただきありがとうございました。百聞は一件にしかずと言いますか、実際にご自身で試してみるのが一番理解が深まるかと思います。ぜひ試してみてください。
参考: