はじめに
設定ファイルを読み書きするconfigparserについて、都度都度ググっていたので自分のために知見をまとめた。
Qiitaユーザーアンケートに回答したらAmazonギフトカード5000円分が当選したのでその恩返しというのもある。
せっかくなので「2024年!初アウトプットをしよう」キャンペーンにも登録したのだが、これ、何かプレゼントないの?
設定ファイルの仕様
configparserで使う設定ファイルはこんな感じになっている。
[DEFAULT]
option1 = default_value_1
option2 = default_value_2
[SECTION1]
option1 = value11
option2 = value12
[SECTION2]
option2 = value22
option3 = value23
[hoge]
がセクション名。たとえが古くて恐縮だが「SFC版ドラクエVの冒険の書は全部で3個」で言うところの一つ一つの冒険の書のことだ。
その下に具体的な設定(キーと値)がある。公式でもキーと言ったりオプションと言ったりしているが違いはよくわからない。
[DEFAULT]
は特別なセクション。デフォルトな設定が保存される。
つまり以下のようになる。[SECTION2]
には無かったoption1
がデフォルト値で設定されている。
まずはこの設定ファイルからこの辞書を得ることを目指そう。
{'DEFAULT': {'option1': 'default_value_1', 'option2': 'default_value_2'},
'SECTION1': {'option1': 'value11', 'option2': 'value12'},
'SECTION2': {'option1': 'default_value_1',
'option2': 'value22',
'option3': 'value23'}}
なお、[DEFAULT]
は無くてもよいし、逆に個別のセクションを持たず[DEFAULT]
だけで運用してもよい。ただしセクションを一つだけで運用するからといってセクションの指定を省略することはできない。
設定ファイルを読み込む
おことわり:自分を納得させるため、非常にまわりくどい表現になっています。
設定ファイルを読み込むにはconfigparser.ConfigParser()でread()
を呼び出す。
import configparser
filename = "config.ini"
config = configparser.ConfigParser()
config.read(filename, encoding="utf-8")
ちなみに戻り値は正常にパースできたファイル名のリスト。
指定するファイルは単一のファイル名でなくファイル名のリストでもいいのだ。
ret = config.read(filename, encoding="utf-8")
print(ret) # ['config.ini']
個別に値を取り出す
各設定値はセクションとオプション名を指定して取り出すことができる。
print(config["SECTION1"]["option1"]) # value11
これは辞書のネスト構造と似ている。
成績 = {"太郎": {"国語":90, "算数":70},
"二郎": {"国語":80, "算数":80},
"三郎": {"国語":60, "算数":100}}
print(成績["太郎"]) # {'国語': 90, '算数': 70}
print(成績["太郎"]["算数"]) # 70
イケてないやり方
get()
を使って内容を取得する方法もあるが、これはレガシーな方法。
ナウでヤングな我々は使わないようにしよう。
print(config.get("SECTION1", "option1")) # value11
全設定値を取り出す
試行錯誤する
辞書に似ているとはいえ、ConfigParserオブジェクトは辞書そのものではない。設定値全体を取り出すにはdict()
で辞書を作る必要がある。
実はそれだけでは不十分なのだが、とりあえずそこまでやってみよう。
print(config)
↓
<configparser.ConfigParser object at 略>
print(dict(config))
↓
{'DEFAULT': <Section: DEFAULT>, 'SECTION1': <Section: SECTION1>, 'SECTION2': <Section: SECTION2>}
config.iniで定義した3個のセクション名をキーに持つ辞書ができたが、その内容はまだ見えていない。セクションの内容(SectionProxy型)についても辞書化する必要がある。
辞書の各キーと値についてループを回すときの構文
for key, value in dict.items():
print(key, value)
をセクションの内容に使えばよい。
つまり、こういうこと
import pprint
config_dict = {}
for section_name, section_proxy in dict(config).items():
config_dict[section_name] = dict(section_proxy)
pprint.pprint(config_dict)
↓
{'DEFAULT': {'option1': 'default_value_1', 'option2': 'default_value_2'},
'SECTION1': {'option1': 'value11', 'option2': 'value12'},
'SECTION2': {'option1': 'default_value_1',
'option2': 'value22',
'option3': 'value23'}}
内包表記でも書ける。
config_dict = {section_name: dict(section_proxy) for section_name, section_proxy in dict(config).items()}
注意事項
設定値はテキスト
configparserはテキストファイルを読んでキーと値を取得している。だからキーも値も文字列となる。数値として扱いたかったら別途int()
やfloat()
する必要がある。自分で作るプログラムだからそれで問題ないはずだ。
実際はconfigparserにはgetboolean()
とかgetint()
とかgetfloat()
といった方法もあるのだが、無理して使う必要はないだろう。
大文字小文字について
オプション名はデフォルトでは大文字小文字の区別を持たず、内部的には小文字で扱われる。
大文字小文字を区別するには
config = configparser.ConfigParser()
config.optionxform = str
とする。これにより
[DEFAULT]
option = default_value
[SECTION1]
OPTION = value1
という内容の設定ファイルを取り込んだ結果は
{'DEFAULT': {'option': 'default_value'},
'SECTION1': {'OPTION': 'value1', 'option': 'default_value'}}
となり、SECTION1
内に大文字のOPTION
と小文字のoption
が共に存在することがわかる。
コメントについて
デフォルトでは#
と;
で始まる行はコメントとして扱われ、設定としては無視される。
詳細は後述。
設定を変更する
辞書として扱うことができるのを理解すれば変更するのも難しくない。
と言いたいところだが、ミスったので詳しく書いておく。
値を取得するのと同様の方法でキーの値を変更することができる。
既存のセクションに対して設定値(キーと値)を追加することもできる。
だが、新たなセクションとその中の設定値をこの方法で追加することはできない。
なぜならば辞書のネストだから。辞書のルールを守って追加しよう。
# 既存のセクションの既存のキーの値を変更する → 可
config["SECTION1"]["option1"] = "new_value11"
# 既存のセクションに新たなキーと値を追加する → 可
config["SECTION1"]["option3"] = "new_value13"
# 存在しないセクションのキーの値を変更する → エラー
# config["SECTION3"]["option1"] = "error"
# 対策1 新たなセクションに空の辞書を設定してからキーと値を追加する
config["SECTION3"] = {}
config["SECTION3"]["option1"] = "new_value31"
# 対策2 新たなセクションに辞書を登録する
config["SECTION4"] = {"option1": "new_value41"}
イケてないやり方
set()
を使って内容を更新する方法もあるが、これはレガシーな方法。
ナウでヤングな我々は使わないようにしよう。
# 既存のセクションの既存のキーの値を変更する → 可
config.set("SECTION1", "option1", "new_value11")
# 既存のセクションに新たなキーと値を追加する → 可
config.set("SECTION1", "option3", "new_value13")
# 新たなセクションにキーと値を追加する → エラー
# config.set("NEW_SECTION", "option1", "error")
# 対策 新たなセクションを作成してからキーと値を追加する
config.add_section("SECTION3")
config.set("SECTION3", "option1", "new_value31")
設定ファイルを書き込む
設定をファイルに保存するにはオープンしたファイルに対し write()
をおこなう。もちろんmode="w"
でだ。mode="a"
で追記すると既存の内容に同一セクションが重複して登録されてしまい、読み込み時にエラーになってしまう。
一部のみ更新するという機能はない。
データベースではなくテキストファイルなのだから仕方がないし、そもそもConfigParserオブジェクトにはread()
した全内容が格納されているのだから一部とか全部とか気にする必要はない。
注意事項
コメントを残す
とはいえ一部のみ更新したいというニーズは理解できる。コメントを残したいからだ。
先述の通りconfigparserは読み込み時にコメント行を無視する。そしてコメントを書き込むことはできない。それでは人がわかりやすく書いたコメント行が消えてしまうではないか。
それでも次のような工夫をすることでコメントを再現することができる。
configparser.ConfigParser()は以下の引数を持つ。
- comment_prefixes コメントの接頭辞。デフォルトでは
('#', ';')
。 - allow_no_value 値を持たないキーのみの設定を許可するか否か。デフォルトでは
False
.。
つまり、コメント行にしたい接頭辞をcomment_prefixesから除外したうえで値を持たない設定を許可すれば、人間にとってはコメントだがconfigparserにとってはキーとして扱われ保存もできるというわけだ。
ただしキーとしての扱いなのでセクションの外つまり先頭の行にはこの方法は使えない。
import configparser
# #はコメントではない扱いにする
config = configparser.ConfigParser(allow_no_value=True, comment_prefixes=";")
# 読み込み
config.read("config_with_comment.ini", encoding="utf-8")
# 変更
config["DEFAULT"]["option"] = "new_value"
# 内容確認
config_dict = {section_name: dict(section_proxy) for section_name, section_proxy in dict(config).items()}
print(config_dict)
# 書き込み
with open("new_config_with_comment.ini", "w", encoding="utf-8") as f:
config.write(f)
[DEFAULT]
# comment
option = value
実行すると、# comment
というキーがNone
という値を持っていることがわかる。
{'DEFAULT': {'# comment': None, 'option': 'new_value'}}
これをwrite()
すると# comment = None
にはならず、普通に# comment
というキー(人間にとってはコメント)のみが保存される。
[DEFAULT]
# comment
option = new_value
ちなみに、あくまでキーという扱いなので、一つのセクションに同じコメントを置くことはできない。
たとえば下のように設定ファイルに装飾を施そうとすると、4行目が2行目と同じキーになるのでエラーになる。かつてベーマガ投稿プログラムを入力して楽しんでいた元少年少女は気をつけよう。
[DEFAULT]
########################################################
# ◯◯プログラム設定ファイル 2024/01/28 by mo256man
########################################################
クラス化するといいよ
configparser.ConfigParser()でread()
をおこなうにあたり、指定したファイルが存在しない場合はエラーにはならず空のデータセットが作られる。この仕様は諸刃の剣だ。
read()
のファイル名が間違っていて設定ファイルを読み込むことができずに空のデータセットが作られ、それを既存の設定ファイルにwrite()
してしまったら…考えるだけでも恐ろしい。
上のサンプルでは変化点を見比べるため読み込むファイルと書き込むファイルを別にしたが、実際にアプリケーションを作って設定ファイルを読み書きする際は同一ファイルになることだろう。
そんなときこそクラスの出番だ。
私も昔はクラスの使い所が理解できなかったものだが、こんな偉そうなことが書けるようになったのだなあ。
サンプルプログラムはやはり湯婆婆
以上の知見を元にサンプルプログラムを作った。
- 設定ファイルが存在しない場合は新たに作られる
- 設定ファイルが存在するときは内容を読み込み、連番を継続する
- データ追加はもちろん削除もできる
という機能を持つので、十分に実用に耐えられるだろう。
最大の欠点は、これ湯婆婆じゃないよねという点だ。
実行結果
契約書だよ。そこに名前を書きな。
? 荻野千尋
フン。荻野千尋というのかい。贅沢な名だねぇ。
今からお前は従業員ナンバー1の尋だ。
契約書だよ。そこに名前を書きな。
? 草壁サツキ
フン。草壁サツキというのかい。贅沢な名だねぇ。
今からお前は従業員ナンバー2のサだ。
契約書だよ。そこに名前を書きな。
? 月島雫
フン。月島雫というのかい。贅沢な名だねぇ。
今からお前は従業員ナンバー3の月だ。
契約書だよ。そこに名前を書きな。
? 荻野千尋
はあ? 荻野千尋だって?
お前は従業員ナンバー1の尋じゃないか。さっさと働きな。
契約書だよ。そこに名前を書きな。
? del 荻野千尋
なぜその隠しコマンドを知っているんだ、尋!
わかった、お前はもう自由だ。今こそ名前を返そう、荻野千尋よ。
契約書だよ。そこに名前を書きな。
? 堀越二郎
フン。堀越二郎というのかい。贅沢な名だねぇ。
今からお前は従業員ナンバー4の堀だ。
契約書だよ。そこに名前を書きな。
?
IndexError: Cannot choose from an empty sequence
作られた設定ファイル
従業員ナンバー1の尋こと荻野千尋は削除されているのがわかる。
[草壁サツキ]
num = 2
new_name = サ
[月島雫]
num = 3
new_name = 月
[堀越二郎]
num = 4
new_name = 堀
ソースコード
折りたたみ
import configparser
import random
class Yubaba():
def __init__(self):
self.config = configparser.ConfigParser()
self.filename = "ybb.ini"
self.encode = "utf-8"
self.config.read(self.filename, encoding=self.encode)
def write(self):
"""ファイルに出力する"""
with open(self.filename, "w", encoding=self.encode) as f:
self.config.write(f)
def get_data(self, section_name):
"""numとnew_nameの値をタプルとして返す"""
return (int(self.config[section_name]["num"]),
self.config[section_name]["new_name"])
def add_section(self, section_name, num, new_name):
"""新たなセクションとその設定値を追加する"""
dict = {"num": num,
"new_name": new_name}
self.config[section_name] = dict
self.write()
def del_section(self, section_name):
"""セクションを削除する"""
self.config.remove_section(section_name)
self.write()
def has_section(self, section_name):
"""セクションの有無のブール値を返す"""
return section_name in self.config.sections()
def get_max_num(self):
"""numの最大値を求める"""
# 三項演算子使って1行で書けるけどさすがに複雑すぎるので場合分けする
sections = self.config.sections()
if sections==[]:
max_num = 0
else:
max_num = max([int(self.config[section]["num"]) for section in sections])
return max_num
def query_name(name):
if ybb.has_section(name):
num, new_name = ybb.get_data(name)
print(f"はあ? {name}だって?")
print(f"お前は従業員ナンバー{num}の{new_name}じゃないか。さっさと働きな。")
else:
num = ybb.get_max_num() + 1
new_name = random.choice(name)
print(f"フン。{name}というのかい。贅沢な名だねぇ。")
print(f"今からお前は従業員ナンバー{num}の{new_name}だ。")
ybb.add_section(name, num, new_name)
def main():
while True:
print("\n契約書だよ。そこに名前を書きな。")
name = input("? ")
if name[:4] == "del ":
real_name = name[4:]
if ybb.has_section(real_name):
_, new_name = ybb.get_data(real_name)
print(f"なぜその隠しコマンドを知っているんだ、{new_name}!")
print(f"わかった、お前はもう自由だ。今こそ名前を返そう、{real_name}よ。")
ybb.del_section(real_name)
else:
query_name(name)
else:
query_name(name)
ybb = Yubaba()
if __name__=="__main__":
main()
終わりに
configファイルの可読性の高さはcsvやjsonと比べると群を抜いている。
これは設定に使うだけではもったいない。多数のデータの保存にも使えることがわかった。
処理速度は遅いかもしれないが(未検証)、辞書にすればpandasで料理できるし悪くないやり方かも知れない。