ちょっと話題の記事

【Python】体当たりで学ぶデコレータとスコープ

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは。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_Xvar_a から var_h に置換して検証していきます。結果として、指定した変数が inner() の中で参照・変更できるよう調節を繰り返します。 inner() では、指定された変数を参照し、変更して、再度参照するという処理を行なっています。GitHubにサンプルコード置いておきますので、よかったらご自身でも試してみてください。

GitHub - TakumiHaruta/decorator_practice: Easy examples for decorators and scopes

一つずつ var_a から検証していきましょう。

目次

var_a

var_a はプログラムの一番最初に定義されるグローバル変数です。 VAR_Xvar_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_aCHANGED を代入する」挙動を意図したとしても、参照する順番が違うため人間の感覚通りには動いてくれません。

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_bouter() 内の関数からでしか参照ができないため、デコレータからは直接参照することができません。デコレータの中で var_b を参照するには、var_bdecorator() のパラメータに渡すか、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_fouter() の引数とパラメータです。 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_ginner() の引数とパラメータです。 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_hinner() の引数に渡してあげると解決します。

# 結果
--- 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() の外に書くか、中に書くか決める必要があります。

globalnonlocal は、宣言した変数をグローバル変数・ノンローカル変数にするものではない

これは個人的に勘違いしていたのですが、 global 宣言や nonlocal 宣言の意味は、宣言した変数に対してスコープ外からも参照・変更を許可することではありません。これらの宣言でやっていることは、 スコープ外にある変数を、スコープ内にある同名のローカル変数と紐付ける ことであり、グローバル変数として存在しない変数に対して global 宣言をしても、グローバル変数としては扱うことはできません。

引数として与えられたオブジェクトに対しても nonlocal 宣言することができる

これが可能なことによって、少しわかりづらいスコープをもつ変数が定義される可能性があります。あまり見ない例かもしれませんが...

最後に

長々とお読みいただきありがとうございました。百聞は一件にしかずと言いますか、実際にご自身で試してみるのが一番理解が深まるかと思います。ぜひ試してみてください。

参考: