pythonプログラムにおける設定ファイル管理モジュール～configparserの使い方と注意点～

# はじめに
アプリケーションを作る際は、アプリ自身の設定ファイルは必要になることが多々あります。
pythonでは設定ファイルを管理する内部モジュールとしてconfigparserがあります。
便利なモジュールですが、実際に使うと色々な注意点が必要なのでその情報をまとめてみました。


# 単純な使い方
まずは以下のconfig.ini(設定ファイル)を読み込みデータを表示させる簡単な例を作成します。
簡単にするためconfig.iniは実行するpythonファイルと同ディレクトリに配置します。
今回は[DEFAULT]セクションの「User」の値を取得します。

```ini:config.ini
[DEFAULT]
User = Taro
Friend = Brother
Age = 20
No = 1
Default_nengo = 昭和
is_Hero = yes

[ULTRAMAN]
User = Hayata
Skil = Spacium beam
Age = 200000
No = 2

[Masked Rider]
User = Takeshi
Frined = Hayato
Machine = Cyclone
Age = 23
```


```python:configparser_sample1.py
# coding: utf-8
import configparser


# --------------------------------------------------
# configparserの宣言とiniファイルの読み込み
# --------------------------------------------------
config_ini = configparser.ConfigParser()
config_ini.read('config.ini', encoding='utf-8')


# --------------------------------------------------
# config,iniから値取得
# --------------------------------------------------
# config.iniの値取得その1
var1 = config_ini['DEFAULT']['User']

# config.iniの値取得その2
var2 = config_ini.get('DEFAULT', 'User')

# config.iniの値取得その3
read_default = config_ini['DEFAULT']
var3 = read_default['User']

# config.iniの値取得その4
read_default = config_ini['DEFAULT']
var4 = read_default.get('User')


# --------------------------------------------------
# 結果表示
# --------------------------------------------------
print('var1 :', var1)
print('var2 :', var2)
print('var3 :', var3)
print('var4 :', var4)
```

```:実行結果
var1 : Taro
var2 : Taro
var3 : Taro
var4 : Taro
```

上記の通り、4つの手法で値を取得可能です。
個人的によく使うのは取得方法その4です。
上記は必要最低限の方法でconfig.iniの値を取得しています。
しかし実際に使用する際は注意点があります。
そのため以下に使い方と注意点について説明します。
尚、本説明ではconfig.iniには常に上記のものを利用します。


# 設定ファイル(config.ini)の読み込みと注意点

設定ファイル(config.ini)を読み込むread()関数は、読み込む対象のファイルが存在するかチェックしません。
存在しない設定ファイルを指定した場合、Noneとなります。

```python:configparser_sample2.py
# coding: utf-8
import configparser


# --------------------------------------------------
# 存在しないiniファイルの読み込み
# --------------------------------------------------
config_ini = configparser.ConfigParser()
config_ini.read('hoge.ini', encoding='utf-8')

# hoge.iniの値取得(存在しないためNone)
read_default = config_ini['DEFAULT']
var = read_default.get('User')

# 結果表示(Noneが表示)
print('var :', var)
```

```:実行結果
var : None
```

よって次のように別途、config.iniが存在するかチェックする必要があります。
(あるいはconfig.iniから取得した値がNoneの場合の対応を記述します)

```python:configparser_sample3.py
# coding: utf-8
import configparser

# ファイルの存在チェック用モジュール
import os
import errno


# --------------------------------------------------
# iniファイルの読み込み
# --------------------------------------------------
config_ini = configparser.ConfigParser()
config_ini_path = 'hoge.ini'

# 指定したiniファイルが存在しない場合、エラー発生
if not os.path.exists(config_ini_path):
    raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), config_ini_path)

config_ini.read(config_ini_path, encoding='utf-8')

# iniの値取得
read_default = config_ini['DEFAULT']
var = read_default.get('User')

# 結果表示
print('var :', var)
```

```:実行結果
FileNotFoundError: [Errno 2] No such file or directory: 'hoge.ini'
```
想定通り、hoge.iniは存在しないため、「raise FileNotFoundError」が実行されます。

余談ですが、read()関数を使ってopen()したファイルはread()関数内でclose()されます。
そのため別途colose処理を実施する必要はありません。

## read_fileの利用

ファイルのopen()処理をconfigparserに任せたくない場合はread_file()関数を使います。
こちらはread()関数と異なり、open()したファイルを引数に渡します。

```python:configparser_sample4.py
# coding: utf-8
import configparser

# ファイルの存在チェック用モジュール
import os
import errno


# --------------------------------------------------
# read_file()関数によるiniファイルの読み込み
# --------------------------------------------------
config_ini = configparser.ConfigParser()
config_ini_path = 'config.ini'

# iniファイルが存在するかチェック
if os.path.exists(config_ini_path):
    # iniファイルが存在する場合、ファイルを読み込む
    with open(config_ini_path, encoding='utf-8') as fp:
        config_ini.read_file(fp)
        
        # iniの値取得
        read_default = config_ini['DEFAULT']
        var = read_default.get('User')

        # 結果表示
        print('var :', var)
else:
    # iniファイルが存在しない場合、エラー発生
    raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), config_ini_path)
```

```:実行結果
var : Taro
```

上記の通り、ファイルのopen()、close()のタイミングは自分で決められます。
(今回はwith句を使用しているためclose()は記述していませんが)
ファイル関連は別クラスで管理したい、または設定ファイル(config.ini)に追記する場合など、
より応用的に使うときに便利な関数です。

# 設定ファイル(config.ini)の値取得方法と注意点

設定ファイル(config.ini)は角括弧([])によって区別されるセクション毎に値が管理されています。
今までは[DEFAULT]セクションの「User」キーの値を取得していたため、「Taro」が得られました。
これを[ULTRAMAN]セクションの「Skil」キーから取得すれば「Spacium beam」が得られます。

余談ですが、存在しないキーから値を取得した場合、エラーではなくNoneとなります。

```python:configparser_sample5.py
# coding: utf-8
import configparser

# ファイルの存在チェック用モジュール
import os
import errno


# iniファイルの読み込み
config_ini = configparser.ConfigParser()
config_ini_path = 'config.ini'

# 指定したiniファイルが存在しない場合、エラー発生
if not os.path.exists(config_ini_path):
    raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), config_ini_path)

config_ini.read(config_ini_path, encoding='utf-8')

# [ULTRAMAN]の「Skil」キーの値を取得
read_ultraman = config_ini['ULTRAMAN']
var = read_ultraman.get('Skil')

# 存在しないキーの値を取得
none_var = read_ultraman.get('Hoge')

# 結果表示
print('var :', var)
print('none_var :', none_var)
```

```:結果表示
var : Spacium beam
none_var : None
```

## [DEFAULT]の取り扱い

[DEFAULT]セクションは特別なセクションです。
他セクションで未設定の値がある場合、[DEFAULT]セクションの設定に準じます。
例えば[ULTRAMAN]セクションには「Friend」キーが未設定のため、[DEFAULT]セクションの「Friend」キーの値である「Brother」が反映されます。
同様に[Masked Rider]セクションは「No」キーが未設定のため、[DEFAULT]セクションの「No」キーの値である「1」が反映されます。

```python:configparser_sample6.py
# coding: utf-8
import configparser

# ファイルの存在チェック用モジュール
import os
import errno


# iniファイルの読み込み
config_ini = configparser.ConfigParser()
config_ini_path = 'config.ini'

# 指定したiniファイルが存在しない場合、エラー発生
if not os.path.exists(config_ini_path):
    raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), config_ini_path)

config_ini.read(config_ini_path, encoding='utf-8')

# [ULTRAMAN]セクションの値取得
# 「Friend」キーは[ULTRAMAN]で未設定のため[DEFAULT]の「Brother」となる
read_ultraman = config_ini['ULTRAMAN']

# [Masked Rider]セクションの値取得
# 「No」キーは[Masked Rider]で未設定のため[DEFAULT]の「No」となる
read_masked_rider = config_ini['Masked Rider']

# 結果表示の体裁
result_str = """[{section_name}]
User   : {user}
Friend : {friend}
No     : {no}"""

# 結果表示
for read_conf in [read_ultraman, read_masked_rider]:
    # [ULTRAMAN]と[Masked Rider]セクションを順次表示
    print(result_str.format(
        section_name=read_conf.name,
        user=read_conf.get('User'),
        friend=read_conf.get('Friend'),
        no=read_conf.get('no')
    ))
    
    print()
```

```:結果表示
[ULTRAMAN]
User   : Hayata
Friend : Brother
No     : 2

[Masked Rider]
User   : Takeshi
Friend : Brother
No     : 1

```

## データ型

get()関数で取得した設定ファイル(config.ini)のデータ型は全て文字列型となります。
一方でgetint()関数等で、特定のデータ型としても取得は可能です。
ですが取得対象データがint型に変換できない場合、getinit()はエラーとなります。
とりあえずget()関数で取得し、データ型の変換と例外処理は別途実施する、という方法も考えられます。
用途によって使い分けるとよいでしょう。

```python:configparser_sample7.py
# coding: utf-8
import configparser

# ファイルの存在チェック用モジュール
import os
import errno


# iniファイルの読み込み
config_ini = configparser.ConfigParser()
config_ini_path = 'config.ini'

# 指定したiniファイルが存在しない場合、エラー発生
if not os.path.exists(config_ini_path):
    raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), config_ini_path)

config_ini.read(config_ini_path, encoding='utf-8')


# --------------------------------------------------
# get()で取得した値は全てstr型となる
# --------------------------------------------------
# [ULTRAMAN]セクションの「No」キー(数字)の値取得
read_ultraman = config_ini['ULTRAMAN']
ultraman_no = read_ultraman.get('No')

# 「No」キーの値のデータ型を表示
print('Get "No" Data Type : ', type(ultraman_no))
print('------------------------------')


# --------------------------------------------------
# getXXX()で取得した値はそれぞれ適応した値となる
# --------------------------------------------------
# getint()で[ULTRAMAN]セクションの「No」キー(数字)の値取得
ultraman_no_getint = read_ultraman.getint('No')

# getboolean()で[ULTRAMAN]セクションの「is_Hero」キー(数字)の値取得
ultraman_is_hero_getboolean = read_ultraman.getboolean('is_Hero')

# getXXX()で取得したデータ型を表示
print('Get "No" Data Type : ', type(ultraman_no_getint))
print('Get "is_Hero" Data Type : ', type(ultraman_is_hero_getboolean))
```

```:結果表示
Get "No" Data Type :  <class 'str'>
------------------------------
Get "No" Data Type :  <class 'int'>
Get "is_Hero" Data Type :  <class 'bool'>
```

## まとめて取得

今まではデータを1つ1つ取得していましたが、まとめて取得する関数も用意されています。

```python:configparser_sample8.py
# coding: utf-8
import configparser

# ファイルの存在チェック用モジュール
import os
import errno


# iniファイルの読み込み
config_ini = configparser.ConfigParser()
config_ini_path = 'config.ini'

# 指定したiniファイルが存在しない場合、エラー発生
if not os.path.exists(config_ini_path):
    raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), config_ini_path)

config_ini.read(config_ini_path, encoding='utf-8')


# --------------------------------------------------
# セクションの一覧取得
# ※DEFAULTセクションは表示されない点に注意
# --------------------------------------------------
sections_list = config_ini.sections()
print(sections_list)
print('------------------------------')


# --------------------------------------------------
# ULTRAMANの要素一覧
# --------------------------------------------------
ultraman_items = config_ini.items('ULTRAMAN')
print(ultraman_items)
# 辞書型に変換可能
print(dict(ultraman_items))
```

```:実行結果
['ULTRAMAN', 'Masked Rider']
------------------------------
[('user', 'Hayata'), ('friend', 'Brother'), ('age', '200000'), ('no', '2'), ('default_nengo', '昭和'), ('is_hero', 'yes'), ('skil', 'Spacium beam')]
{'user': 'Hayata', 'friend': 'Brother', 'age': '200000', 'no': '2', 'default_nengo': '昭和', 'is_hero': 'yes', 'skil': 'Spacium beam'}
```
上記の通り、セクション一覧、特定セクションのキーと値一覧が取得できます。
ただしitems()で取得するキーと値のペアには、[DEFAULT]セクションの値が含まれている点に注意してください。

# 参考リンク

* configparserのマニュアル
https://docs.python.org/ja/3/library/configparser.html
公式マニュアルです。基本的な使い方が一通り載っています。
<br>

* Closing file opened by ConfigParser
https://stackoverflow.com/questions/990867/closing-file-opened-by-configparser
(補足)https://github.com/python/cpython/blob/3.7/Lib/configparser.py
上述したconfig_ini.read()をcloseする必要がないことを上記のQAサイトで知りました。
現在はソースコードが変更されており、with句でopenしているためcloseする必要はありません。
詳細は(補足)のread()関数における[695～696行](https://github.com/python/cpython/blob/3.7/Lib/configparser.py#L695-L696)を御覧ください。
<br>

* Pythonでファイルが存在するかどうかを確認する方法
https://www.headboost.jp/python-file-existance/
ファイルの存在チェックやwith句によるopen()方法についてまとめられています。
ファイルの読み書きをする場合、目を通しておくと参考になります。
<br>

* Pythonのopen関数のencoding引数は必須でもいいんじゃない
https://takeg.hatenadiary.jp/entry/2018/02/02/115754
ファイルを開く際、環境によってはエラーとなります。
その原因は上記URL先で解説されているエンコーディングに依るものです。
<br>

* [Python]ConfigParserのitems()の結果を辞書で受け取りたい
https://blog.aoshiman.org/entry/113/
設定ファイル(config.ini)の特定セクションの値をまとめて取得する方法の参考にしました。
辞書型への変換についても触れています。