マジックナンバーを消す
吉田 航
2022-02-23 02:45:27
モチベーション
以下の 2 つを同時に満たすことを目指します.
- 複数の実験で共通するパラメータを指定するテンプレートを作る.
- コマンドライン引数でパラメータの変更ができるようにする.
前者はすべての実験のパラメータを統一的に管理するため, 後者は実行時の一時的なパラメータの変更や, シェルスクリプトでの引数指定を可能にするためです.
期待する出力結果
exp_a.py
と exp_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 行なので, 作業量的にはまあ許容範囲内...?