Python パッケージの Config を簡単に管理・反映する方法

## 概要

Python スクリプトを実行中、別途用意した設定ファイルを読み込むことにより内部パラメータを更新します。


## 問題点として

外部設定ファイルを用意して、実行時に読み込み内部に反映するための処理を都度自前で書くのは手間がかかります。
また、設定ファイルが JSON だと編集や読みやすさに分かりづらいところがあります。


## 実現するための手段として

外部モジュール [Traitlets](https://github.com/ipython/traitlets) を使用することにより、設定ファイルの読込みと内部への反映を行います。
これは [Jupyter Notebook](http://jupyter.org/) で採られている手順と同じものになります。


## サンプル
[サンプルプログラム](https://github.com/smn-ailab/tshimura_qiita/tree/master/config) を用意しました。
実際に動作するスクリプトとしてご参考ください。


## 事前の準備や条件など

- この記事では Python 3.6 を使用しています。


## 設定ファイルについて

設定ファイルは Python スクリプトです。
下記は Jupyter Notebook の設定ファイルの一部抜粋です。

```Python
# Configuration file for jupyter-notebook.

## The date format used by logging formatters for %(asctime)s
#c.Application.log_datefmt = '%Y-%m-%d %H:%M:%S'

## The Logging format template
#c.Application.log_format = '[%(name)s]%(highlevel)s %(message)s'
```


## 設定ファイルの書き方

設定ファイルは `config.py` のような任意の名称の Python スクリプトに次のような記述をします。

```Python
c.クラス名.変数名 = 設定したい値
```

c は唐突に現れますが、 `Traitlets` の `Config` インスタンスを表しています。

(サンプルプログラム抜粋)

```Python
# CPU
c.Settings.cpu = "CORE i7"
# Memory (GB)
c.Settings.memory = 16
# USB types
c.Settings.usb_types = ["USB 3.0", "USB Type-C"]
```

この設定ファイルは後述するクラス `Settings` の設定値が書かれています。


## 設定内容を反映させるための準備

- 設定できる変数の型と初期値を予め指定しておく必要があります。
- 設定できる変数を持つクラスは `Configurable` を継承している必要があります。
- 型と初期値の後に `.tag(config=True)` を加えることで設定可能になります。
- 使用できる型には `Int`, `Long`, `Float`の他、様々な種類があります。詳しくは[Traitlets公式ドキュメント - Trait Types]( http://traitlets.readthedocs.io/en/stable/trait_types.html) を参照してください。

(サンプルプログラム抜粋)

```Python
class Settings(Configurable):
    """設定値を保持するクラス."""

    # 保持しているパラメータ.
    # これらのパラメータは外部設定ファイルから更新できる.
    cpu = Unicode("CORE i5").tag(config=True)
    memory = Int(8).tag(config=True)
    usb_types = List(["USB 2.0", "USB 3.0"]).tag(config=True)
```

- ここでは`Configurable`を継承した`Settings`クラスは更新可能な変数`cpu`, `memory`, `usb_types` を持ちます。
- `List` のようなコンテナ型を指定することもできます。
- 文字列は `Unicode` のような指定の仕方になります。


## 設定ファイルの読込みと反映

- 設定ファイルの読込みには Traitlets の `PyFileConfigLoader` を使用します。
- 設定の反映は継承元クラス `Configurable` の `update_config` により実現します。
- 例えば設定ファイルに `c.Sample.value1 = 100` があった場合は、クラス `Sample` の変数 `value1` の値が 100 に更新されます。

(サンプルプログラム抜粋)

```Python
p = Path(CONFIG_FILE)
if p.exists():
    # 設定ファイルが存在すれば読み込む.
    loader = PyFileConfigLoader(str(p))
    c = loader.load_config()
    # 自身のパラメータを更新する.
    self.update_config(c)
```

- ここでは設定ファイル `CONFIG_FILE(=config.py)` があれば読み込み、自クラスのパラメータを更新します。
- 設定ファイルによる更新がない場合、コード中に記述された初期値が使用されます。


## 結果として

これにより実行中に外部設定ファイルによるパラメータの更新ができるようになりました。


## 追記

### 設定ファイルの自動生成

- Traitlets には設定ファイルの内容を自動生成する機能があります。
- Configurable の代わりに Application クラスを継承することにより実現できます。
- generate_config_file() を実行すると、設定ファイルの内容を得ることができます。
- ただ、得られる設定ファイルには継承元の Application クラスの情報が含まれてしまいます。

(サンプルプログラム抜粋)

```Python
class Settings2(Application):
    cpu = Unicode("CORE i3").tag(config=True)
    memory = Int(2).tag(config=True)
    usb_types = List(["USB 1.1", "USB 2.0"]).tag(config=True)

if __name__ == "__main__":
    s2 = Settings2()
    ret = s2.generate_config_file()
```

自動生成された設定ファイルの内容
(Applicationクラスの内容も生成される。)

```Python
# Configuration file for application.
#------------------------------------------------------------------------------
# Application(SingletonConfigurable) configuration
#------------------------------------------------------------------------------
## This is an application.
## The date format used by logging formatters for %(asctime)s
#c.Application.log_datefmt = '%Y-%m-%d %H:%M:%S'
## The Logging format template
#c.Application.log_format = '[%(name)s]%(highlevel)s %(message)s'
## Set the log level by value or name.
#c.Application.log_level = 30

#------------------------------------------------------------------------------
# Settings2(Application) configuration
#------------------------------------------------------------------------------
## This is an application.
##
#c.Settings2.cpu = 'CORE i3'
##
#c.Settings2.memory = 2
##
#c.Settings2.usb_types = ['USB 1.1', 'USB 2.0']
```


## 参考
[Traitlets](http://traitlets.readthedocs.io/en/stable/index.html)