LoginSignup
5
1

More than 3 years have passed since last update.

オレオレデザインパターン:Glocal Variable

Last updated at Posted at 2020-03-05

概要

Pythonのライブラリでたまに見る、「withの中でのみアクセスできるグローバル変数」という設計パターンに、オレオレの名前をつけてまとめてみた。

すでに名前がついてたらすいません。

例題

ある設定に基づき実験(関数experiment)を行うプログラムを書く。

実験は複数の関数(first second)に分割して記述されており、どちらも設定を参照してある実験操作を行う。

config0config1に基づき二回実験を行いたい。

ストレートに書くとこう。

# 本当はもっといろいろ設定がある
config0 = {
    "id": 0
}


config1 = {
    "id": 1
}


def first(conf):
    # なんかする
    print(f"{conf['id']}: first")


def second(conf):
    # なんかする
    print(f"{conf['id']}: second")


def experiment(conf):
    first(conf)
    second(conf)


def main():
    experiment(config0)
    experiment(config1)


main()

ただこの書き方だと、プログラムが複雑になったとき、設定をバケツリレーしていくのがやや面倒。なしですませられないか?

一つの解法はグローバル変数を使うことだが……。

conf = config0


def first():
    print(f"{conf['id']}: first")


def second():
    print(f"{conf['id']}: second")


def experiment():
    first()
    second()


def main():
    global conf
    experiment()
    conf = config1
    experiment()


main()

あきらかにこれはやばやば。

  • グローバル変数を使ったことで、変数に処理がどう依存するか追跡が難しくなる。
  • 上に加え、グローバル変数を途中で変化させていることで、状態の変化過程が追跡しきれずバグになりがち
    • 例えば今回の場合、mainを呼び出した後confconfig1になってることを忘れて、config0のつもりで再度mainを実行したりするとやばい

パターンの導入

バケツリレーを避け、グローバル変数の導入も避けたいということで、その中間的な書き方としてGlocal Variableパターンを紹介する。

config.py
from contextlib import contextmanager


_config = None
_initialized = False


@contextmanager
def configure(data):
    global _config
    global _initialized
    before = _config
    before_initialized = _initialized
    _config = data
    _initialized = True
    try:
        yield
    finally:
        _config = before
        _initialized = before_initialized


def get_config():
    if _initialized:
        return _config
    else:
        # 本当はもうちょっと真面目に例外投げるべき
        raise RuntimeError
from config import get_config, configure


def first():
    print(f"{get_config()['id']}: first")


def second():
    print(f"{get_config()['id']}: second")


def experiment():
    first()
    second()


def main():
    with configure(config0):
          experiment()
    with configure(config1):
          experiment()


main()
  • バケツリレーは避けることができた
  • グローバル変数に比べると安全
    • configureのコンテキストの中でしか変数が使えないので、自由度に制限がある
    • configを直接変化させる術がなく、withを通してしか設定できない
      • withの前後で必ずconfigが初期化・解放されるので、わけのわからない値が残っていてバグを起こす心配がない
    • ただし、「スコープ」(withの中)以外でget_configを呼び出しても、静的解析でエラーは拾えない

このように、

  • withの前後でグローバル変数を設定・初期化し
  • そのグローバル変数を読み出す関数を用意する

パターンをGlocal Variableと呼ぶことにする。

どういう時に使うか?

  • 多くの関数で共有したいデータが存在する
    • 面倒くさくないならバケツリレーを使えばいい
  • そのデータを動的に決める・変える需要がある
    • なければグローバル変数にしたほうがシンプル

実例

Pythonのライブラリではいくつか使われている。

  • 深層学習パッケージのmxnetでは、行列を計算するコンテキスト(CPU, GPU)をGlocal Variableで設定できる
  • Webフレームワークのflaskでは、リクエストパラメータをグローバル変数のように参照できるが、コード上で設定する場合はGlocal Variableパターンを使う

Racketだと、parametrizeというシンタクスが存在し、これが汎用のGlocal Variable機能を提供する。

バリエーション

デフォルト値

初期値をあらかじめ決めることもできる。先ほど言及したmxnetでは、CPUでの計算がデフォルト値になっている。

変更

セッターも用意すれば、with内でGlocal Variableを変更することもできる。

param.py
_param = None
_initialized = False


@contextmanager
def parametrize(data):
    global _param
    global _initialized
    before = _param
    before_initialized = _initialized
    _param = data
    _initialized = True
    try:
        yield
    finally:
        _param = before
        _initialized = before_initialized


def set_param(data):
    if _initialized:
        global _param
        _param = data
    else:
        raise RuntimeError


def get_param():
    if _initialized:
        return _param
    else:
        raise RuntimeError
from param import parametrize, set_param, get_param

with parametrize(3):
    with parametrize(4):
        print(get_param())
        set_param(2)
        print(get_param())
    print(get_param())
get_param()

# 4
# 2
# 3
# RuntimeError

読み取りしかできない場合に比べると、状態を追う努力が必要な分危険度は高まる。

だが状態変化の影響はwith内に限定できるので、グローバル変数に比べれば安全。

パーサなどを書く場合は、「まだ読んでいない文章」をGlocal Variableにして、少しずつ先頭から消費していくような書き方をすると、バケツリレーなしで書けて便利かも。

注意

Glocal Variableの値は、getterが記述された場所ではなく、実行されたタイミングで決まることに注意しよう。

def get_print_config():
    # この2ではなく
    with configure(2):
        def print_config():
            print(get_config())
        return print_config


print_config = get_print_config()
# この3が参照される
with configure(3):
    print_config()
# 3

備考

元々はPythonでも状態モナドのdo記法みたいな感じで書けないかなーと思って、flaskやmxnetのことを思い出し、こういうパターンあるなと気づいたのだった。

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1