[PYTHON] Ore Ore Design Pattern: Glocal Variable

Overview

I named the design pattern "global variables that can only be accessed in with" that I sometimes see in the Python library.

I'm sorry if it already has a name.

example

Write a program that performs an experiment (function ʻexperiment`) based on a certain setting.

The experiment is divided into multiple functions (first`` second), and both perform experimental operations with reference to the settings.

I want to experiment twice based on config0 and config1.

Write it straight like this.

#There are actually more settings
config0 = {
    "id": 0
}


config1 = {
    "id": 1
}


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


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


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


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


main()

However, with this writing method, it is a little troublesome to bucket-relay the settings when the program becomes complicated. Can't you do without it?

One solution is to use global variables ...

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()

Obviously this is crazy.

Introduction of patterns

Since we want to avoid bucket relay and avoid introducing global variables, we will introduce the Glocal Variable pattern as an intermediate way of writing.

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:
        #Actually, I should throw an exception a little more seriously
        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()

in this way,

Let's call the pattern ** Glocal Variable **.

When do you use it?

Illustration

It is used in some Python libraries.

With Racket, there is a syntax called parametrize, which provides general-purpose Glocal Variable functionality.

variation

Default value

The initial value can also be determined in advance. In mxnet mentioned earlier, the calculation on the CPU is the default value.

Change

If you also have a setter, you can change the Glocal Variable in with.

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

Compared to the case where only reading is possible, the risk is higher because of the effort required to track the state.

However, the effect of state changes can be limited to within with, so it is safer than global variables.

When writing a parser etc., it may be convenient to write "sentences that have not been read yet" as Glocal Variable and gradually consume from the beginning so that you can write without a bucket relay.

Caution

Note that the value of Glocal Variable is determined when the getter is ** executed, not where it is ** described **.

def get_print_config():
    #Not this 2
    with configure(2):
        def print_config():
            print(get_config())
        return print_config


print_config = get_print_config()
#This 3 is referenced
with configure(3):
    print_config()
# 3

Remarks

Originally, I thought that I could write it in Python as if it were a state monad do notation, so I remembered flask and mxnet and realized that there was such a pattern.

Recommended Posts

Ore Ore Design Pattern: Glocal Variable
Design Pattern #Builder
Design Pattern #Adapter
Design Pattern #Observer
Design Pattern #Facade
Design Pattern #Strategy
Design Pattern #Singleton
Design Pattern #Proxy
Design Pattern #Factory Method
Design Pattern #Template Method
Python Design Pattern --Template method
[Gang of Four] Design pattern learning
GoF java design pattern rough summary