python+Configparser

pythonで各種の設定データを外部化するために、configparserを使う機会が多いので、
どうにか、いい方法がないかなと考えたコードを記載します。

* 標準ライブラリのconfigparser  
* デザインパターンのSingleton  
* 組込関数のsetattrとproperty  

を使用したクラスになります。

最後にコードを載せていますので、解説不要な方は最後に飛んでください。

# configparser
iniファイル扱うための標準のクラスです。
pythonでiniファイルを取り扱う場合は、これ一択ぐらいで使用しています。
## iniファイルの構成例
iniファイルの中にはセクションという1つのグループの中にキーと値のセットが1つ以上あります。
今回は下記のiniファイルを前提に説明します。
test1.ini

```config
[hoge]
a = 0
b = 1

[fuga]
a = 10
b = 11
c = 12
```

test2.ini

```config
[wkwk]
a = 0
```



# Singleton
GoFのデザインパターンの生成に関するパターンのひとつである。Singltonを使用します。

## Singletonを使用するメリット
Singletonを使用すると判断した理由として、
**iniファイルはリソース**
ということが、大きな決め手です。  

* iniファイルの情報をもつインスタンスは1つの方が好ましい  
* iniファイルの情報をもつインスタンスが複数だと、メモリの無駄遣いで冗長  

## Singletonのコード
Singleton用のテンプレート

```python
class Singleton:
    _instance = None
    _init_lock = False

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self, test):
        if not self._init_lock:
            self.initialize(test)

    def initialize(self, test):
        self._init_lock = True
        self.test = test

    @classmethod
    def get_instance(cls):
        return cls._instance

    @classmethod
    def clear_init_lock(cls):
        cls._init_lock = False
```

pythonでのSingletonの実装は色んな書き方があります。
個人的には、上記のコードが1番使いやすく分かり易いかなと思っています。

### コード解説
このコードについては、別途解説しようかなと思うので、現在は省略させていただきます。

# setattrとproperty
pythonの組込関数であるsetattrとpropertyを使用します。

## setattrとpropertyを使用するメリット
iniファイルは実装規模によって、キーと値の数が多かったり、少なかったりします。
多い場合など、そのキーに対応する属性を追加するのは、大変手間です。
その手間を省き、属性追加を自動で行うために、setattrを使用します。
また、propertyを使うことで、privateな変数のようにふるまえるようにします。

## setattrとpropertyのコード
setattrとproperty用のコード

```python
def add_property(cls, name, value=None, read=True, write=True):
    field_name = f'_{cls.__class__.__name__}__{name}'
    setattr(cls, field_name, value)
    fget = (lambda cls: getattr(cls, field_name, value)) if read else None
    fset = (lambda _, value: setattr(cls, field_name, value)) if write else None
    setattr(cls.__class__, name, property(fget=fget, fset=fset))
```

今回は属性を追加するためにpropertyを使用します。
fsetとfgetには同じ組込関数のgetattrとsetattrを使用しています。

### コード解説
**add_property**に与えるオプション引数の設定については下記のとおりです。

* readがTrueなら読み取り可、Falseなら読み取り不可  
* writeがTrueなら書き込み可、Falseなら書き込み不可  
* valueについては設定されないとNoneオブジェクトがデフォルト値として設定されます。

# 最強のコード
これまでで説明した、configparser、Singleton、proptery
を用いて、最強のコードを書いていきます。

## ConfigParser特化クラス

```python
import configparser

def add_property(cls, name, value=None, read=True, write=True):
    field_name = f'_{cls.__class__.__name__}__{name}'
    setattr(cls, field_name, value)
    fget = (lambda cls: getattr(cls, field_name, value)) if read else None
    fset = (lambda _, value: setattr(cls, field_name, value)) if write else None
    setattr(cls.__class__, name, property(fget=fget, fset=fset))

class ConfigParserBase(object):
    _instance = None
    _init_lock = False

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self, config_path):
        if not self._init_lock:
            self.initialize(config_path)

    @classmethod
    def get_instance(cls):
        return cls._instance

    @classmethod
    def clear_init_lock(cls):
        cls._init_lock = False

    def initialize(self, config_path, encoding='utf-8'):
        self._init_lock = True
        self._config_path = config_path
        self._config_file = configparser.ConfigParser()
        self._config_file.optionxform = str
        self._config_file.read(config_path, encoding=encoding)

        for section_name in self._config_file.sections():
            section = self._config_file[section_name]
            print(type(section_name, (object,), dict())())
            add_property(self, section_name, value=type(section_name, (object,), dict())())
            for item, value in section.items():
                add_property(getattr(self, section_name), item, value=value, write=False)

```

## 使い方
僕が考えた最強のConfigParser特化クラスの使い方は下記となります。

```python
class Test1(ConfigParserBase):
    def __init__(self, config_path):
        super().__init__(config_path)

class Test2(ConfigParserBase):
    def __init__(self, config_path):
        super().__init__(config_path)

if __name__ == '__main__':
    test1 = Test1('test1.ini')
    test2 = Test2('test2.ini')
    print('---------------------')
    print(test1.hoge.a) # 0
    print(test1.hoge.b) # 1
    print(test1.fuga.a) # 10
    print(test1.fuga.b) # 11
    print(test1.fuga.c) # 12
    print(test2.wkwk.a) # wkwk
    print('---------------------')
    get_instance = Test1.get_instance()
    print(get_instance.hoge.a) # 0
    print(get_instance.hoge.b) # 1
    print(get_instance.fuga.a) # 10
    print(get_instance.fuga.b) # 11
    print(get_instance.fuga.c) # 12
```

# まとめ
僕が考えた最強のConfigParser特化クラスとその使い方について解説をしました。
このコードの何がいいかというと

* iniファイルの情報を無駄に持つことがなくなる。  
* クラス化を行うことで、iniファイルの情報を一つに集約ができる。  
* 最初にインスタンス作成すれば、**get_instance**でインスタンスを取得できるので、iniのパスをいろんなところに持たせなくて済む。  
* iniのキーに対応した属性の追加を自動で対応してくれる。  
* iniのセクションも属性としてアクセスができる。  
* iniの値はread専用である。  

などなどのメリットがあります。個人的な見解ですが。

個人的にはこのコードを使用していく方針ですが、
これ以外にもいい方法があれば、ご連絡いただけると助かります。

quiitaの記事初投稿してみた。