Pythonのデコレータを利用したグローバル変数の初期化


はじめに

githubでpipのリポジトリを見ていたら、面白いデコレータの使い方をしていたので共有します。

TL;DR

  • デコレータは関数を修飾するためだけでなく、関数を実行するために用いることもできる。
  • 本題

importした時のグローバル変数の値

まず、以下のようなパッケージをimportした時にグローバル変数がどのような値をとるのでしょうか。関数spameggもグローバル変数global_variableを更新するためだけの関数です。

mypackage/__init__.py
GLOBAL_VARIABLE = 0

def spam():
    global_variable = 1
    globals().update(locals())

def egg():
    global global_variable
    global_variable = 2
main.py
import mypackage
if __name__ == "__main__":
    print(mypackage.global_variable)

関数を定義しているだけで実行していないので、global_variableの値は宣言時から変化していません。実行結果はもちろん以下のようになります。

$python3 main.py
0

では、mypackage/__init__.pyの末尾で関数を呼び出してみるとどうなるでしょうか。

mypackage/__init__.py
GLOBAL_VARIABLE = 0

def spam():
    global_variable = 1
    globals().update(locals())

def egg():
    global global_variable
    global_variable = 2

spam()
egg()

この場合、関数が呼ばれてglobal_variableの値がspam内でもegg内でも上書きされるので、実行結果は以下のようになります。

$python3 main.py
2

しかし、この方法だと、実行したい関数の数が増えるとその分だけ呼び出すためのコードを書かねばならず、面倒ですね。そこで、デコレータの出番です。

デコレータの内部で関数を実行する

ここからが本題です。さっそくコードを見ていきましょう。以下のようなcall_aside関数を考えましょう。

mypackage/__init__.py
global_variable = 0

def call_aside(f, *args, **kwargs):
    f(*args, **kwargs)
    return f

@call_aside
def spam():
    global_variable = 1
    globals().update(locals())

@call_aside
def egg():
    global global_variable
    global_variable = 2
main.py
import mypackage
if __name__ == "__main__":
    print(mypackage.global_variable)

なんと実行結果は、

$python3 main.py
2

のようになります!最初、こうなる理由が分からなくて、結構悩みました。しかし、仕組みは分かると単純です。call_aside関数に注目すると、

def call_aside(f, *args, **kwargs):
    f(*args, **kwargs) # <- execute f
    return f

というように、内部で関数を呼び出しています。したがって、call_asideで修飾しただけで、mypackageがimportされた時点でspameggが実行さるので、上のようにglobal_variableの値が変化したというわけです。これなら、importされた時点で実行したい関数の数が増えても装飾するだけで済みますね

補足

Pythonのデコレータについて調べると、「ある関数の前後に処理を追加する」「前後に処理を追加した関数を返す」ためにデコレータを用います、とデコレータを解説しているものが多いと思います。

def decorator(f):
  def wrapper(*args, **kwargs):  
    print("前処理")
    f(*args, **kwargs)
    print("後処理")
  return wrapper

このような使い方だと、wrapper関数は定義されただけで実行されていません。このデコレータで関数を装飾しても、装飾された関数を呼び出すまでは、グローバル変数の値を更新する等の影響は及ぼしません。

参考

pypa/pip/blob/master/src/pip/_vendor/pkg_resources/__init__.py

最後に

ある程度Pythonを読み書きできるようになったら、CPythonPypaなど公式のgithubを眺めてみてください。そこには、ツヨツヨの方達のコードが無限にあるので、新たな発見があったり、参考になるコードがあったりすると思います。