1G
マジックナンバーを消す
知見

モチベーション

以下の 2 つを同時に満たすことを目指します.

  1. 複数の実験で共通するパラメータを指定するテンプレートを作る.
  2. コマンドライン引数でパラメータの変更ができるようにする.

前者はすべての実験のパラメータを統一的に管理するため, 後者は実行時の一時的なパラメータの変更や, シェルスクリプトでの引数指定を可能にするためです.

期待する出力結果

exp_a.pyexp_b.py の 2 つの実験ファイルがあるとします. この 2 つの実験について, デフォルトのパラメータを用いた場合と, 一時的なパラメータの変更を行った場合の実行結果は以下のようになります.

$ python exp_a.py
[[[ Settings ]]]
   common=4 
   hoge=64
   foo=True
------------------------------------------------------------
以下に処理が続く...

$ python exp_a.py --hoge 128
[[[ Settings ]]]
   common=4 
   hoge=128
   foo=True
------------------------------------------------------------
以下に処理が続く...
$ python exp_b.py
[[[ Settings ]]]
   common=4 
   hoge=32
   foo=False
   log_file=./log/exp_b.log
------------------------------------------------------------
以下に処理が続く...

$ python exp_b.py --log_file ./log/exp_20220222.log
[[[ Settings ]]]
   common=4 
   hoge=32
   foo=False
   log_file=./log/exp_20220222.log
------------------------------------------------------------
以下に処理が続く...

必要なファイル

utils.py

実験コード内の parse_args() を記述する際に必要になる関数群.

import argparse
import configparser
import errno
import os
from collections import defaultdict, namedtuple


def get_arg(*flags, args=None, **kwargs):
    p = argparse.ArgumentParser()
    p.add_argument(*flags, **kwargs)
    ns, _ = p.parse_known_args(args)
    return next(getattr(ns, d) for d in dir(ns) if not d.startswith('_'))


def evaluate_var(var):
    evaluated = None
    try: 
        evaluated = eval(var)
    except: 
        evaluated = var
    return evaluated


def convert_namedtuple(**kwargs):
    d = defaultdict(None)
    for arg in kwargs.keys():
        d[arg] = get_arg('--'+arg)
    Args = namedtuple('Args', sorted(d.keys()))
    for k, v in d.items():
        d[k] = kwargs[k] if v is None else evaluate_var(v)
    return Args(**d)


def get_config_data(key):
    config_ini = configparser.ConfigParser()
    config_filepath = './config.ini'
    if not os.path.exists(config_filepath):
        raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), config_filepath)
    config_ini.read(config_filepath, encoding='utf-8')
    return config_ini[key]


def show_args(args):
    print("[[[ Settings ]]]")
    l = str(args).lstrip('Args(').rstrip(')').split(',')
    for arg in l:
        print("  ", arg.lstrip(' '))
    print("-"*60)

config.ini

パラメータの定義ファイル. セクション内で指定した値を利用する. セクション内で値を指定されていない変数については, [DEFAULT] セクション内の値を用いる.

[DEFAULT]
COMMON=4
HOGE=32
FOO=True
LOG_FILE=./log/example.log

[EXP_A]
HOGE=64

[EXP_B]
FOO=False
LOG_FILE=./log/exp_b.log

実験コードの例

exp_a.py

from utils import convert_namedtuple, evaluate_var, get_config_data, show_args


def parse_args(section):
    config = get_config_data(section)
    args = convert_namedtuple(
        # main() 関数内で用いる変数を列挙 
        common=evaluate_var(config.get('COMMON')),
        hoge=evaluate_var(config.get('HOGE')),
        foo=evaluate_var(config.get('FOO'))
    )
    return args


def main(args):
    common = args.common
    hoge = args.hoge
    foo = args.foo

    # この部分に処理を記述    


if __name__ == '__main__':
    args = parse_args('EXP_A')  # 実験名 (config.ini のセクション名) を指定
    show_args(args)
    main(args)

exp_b.py

from utils import convert_namedtuple, evaluate_var, get_config_data, show_args


def parse_args(section):
    config = get_config_data(section)
    args = convert_namedtuple(
        common=evaluate_var(config.get('COMMON')),
        hoge=evaluate_var(config.get('HOGE')),
        foo=evaluate_var(config.get('FOO')),
        log_file=evaluate_var(config.get('LOG_FILE'))
    )
    return args


def main(args):
    common = args.common
    hoge = args.hoge
    foo = args.foo
    log_file = args.log_file

    # この部分に処理を記述    


if __name__ == '__main__':
    args = parse_args('EXP_B')
    show_args(args)
    main(args)

正直使わない変数をロードしてもいいと考えると, parse_args()utils.py に入れてしまった方が賢いかも. 1 つ変数追加するごとに記述する量は 2 行なので, 作業量的にはまあ許容範囲内...?