2022年11月: 新たに仲間に加わったTOMLパーサー(kadowaki)¶
門脇です(@satoru_kadowaki)。 11月の「Python Monthly Topics」は、Python 3.11で新しく標準ライブラリに追加された「tomllib」モジュールについて解説します。
本題の前に、ご存知の方も多いと思いますが、10月24日、ついに Python 3.11がリリース されました! 先月のPython Monthly Topicsでも紹介した非同期I/Oタスクグループや新しい例外処理といった新機能の他にも、高速化や例外を正確に示すTracebackなど、今回のリリースでも注目の改善点が多く、それらをいち早く把握しておきたいところです。 今回ご紹介するtomllibモジュールも新機能の1つですが、すでに馴染みがある人も、今まであまり使ったことがない人も、この機会に目をとおしてみてください。
TOMLとは¶
TOML(Tom's Obvious Minimal Language) は、設定ファイルフォーマットの1つで、可読性が高く「ミニマル」であることを目指して作られました。 TOMLの主な仕様は以下の通りです。
構成要素は、キー(Key) と 値(Value) の組からなり、キーと値は 等号(=) で区切られる
値は、文字列、整数、浮動少数点数、ブーリアン、日時、配列もしくはインライン・テーブルなどのデータ型を扱うことができる
null (または nil)タイプは存在しない
大文字と小文字を区別(ケース・センシティブ)するテキストファイルである
TOMLファイルはUTF-8でエンコードされている必要がある
ハッシュ記号
#
から行末までをコメントとして扱う
主な型を使用したTOMLのサンプルを以下に示します。
# ハッシュ記号以降はコメントです
key = "value" # 左辺にキー、イコールで右辺に値を指定します
# 文字列以外の主なデータ型
month = 11 # 整数(Integer)
pi = 3.14 # 浮動小数点数(Float)
is_example = true # ブーリアン(Boolean): true/false
created_at = 2022-11-01T22:38:34+09:00 # オフセット付き日時(Offset Date-Time)
values = [1, 2, 3] # 配列(Array)
その他の細かい仕様については 詳細仕様 ページに記載がありますので、興味のある方は読んでみてください。 同様のファイルフォーマットとして YAML や JSON もありますが、よりシンプルなTOMLは多くのプログラミング言語でパーサーが実装されています。
TOMLがYAMLやJSONとどのように違うのか、それぞれの構造の違いを簡単にみてみましょう。 それぞれのフォーマットにおける大きな違いは以下のようなものがあります。
特徴 |
YAML |
JSON |
TOML |
---|---|---|---|
キーと値の区切り文字 |
|
|
|
コメントが書ける |
書ける |
書けない |
書ける |
日付型の有無 |
有り |
無し |
有り |
null値の有無 |
有り |
有り |
無し |
構造化データの表現 |
インデントを使用 |
データの最初と最後を波括弧で括る |
角括弧やドットを使用 |
いずれの形式でも簡単なデータを表す場合はそれほど違いがありません。 同一のデータがそれぞれのフォーマットでどのように表現されるか、簡単な例を使用して見比べてみます。
TOMLでは以下のように簡潔に表現できます。
[app]
app_name = "example"
environment.NAME = "sandbox"
environment.VERSION = "0.0.1"
volumes = ["vol1", "vol2"]
YAMLでは以下のようになります。 このデータにおいてはシンプルで可読性も悪くありません。 しかし、YAMLはインデントを使用してデータ構造を表現することから、ネストが深くなってしまうことがあります。 ネストが深くなると可読性が悪くなり、インデントがずれてパースエラーや意図しない階層構造で読み込まれてしまうことがあるので注意が必要です。
app:
app_name: exsample
environment:
NAME: sandbox
VERSION: 0.0.1
volumes: # volumes: [vol1, vol2] のような表現も可能
- vol1
- vol2
JSONも以下のようにこのデータについてはシンプルです。 JSONは波括弧でデータの括りを表現するため、複雑な構造になるほど波括弧の位置に注意して書く必要があります。 APIなどプログラム同士の情報のやりとりによく使用されるJSONですが、大きなデータを手入力で書くのは大変な作業です。
{
"app": {
"app_name": "example",
"environment": {
"NAME": "sandbox",
"VERSION": "0.0.1"
},
"volumes": [
"vol1",
"vol2"
]
}
}
PythonとTOML¶
Python 3.11では、TOMLフォーマットをパースする tomllib が標準ライブラリとして提供されることになりました。 これは、PEP 680 - tomllib: Support for Parsing TOML in the Standard Library で提案され、実装されました。 PEP 680の主な内容を簡単にまとめると、以下のような記載があります。
TOMLはPythonのパッケージングに選ばれているファイルフォーマットであり、 下記のPEPで提案され実装されている
TOMLがPythonで標準サポートされることにより、ベンダー依存のあるTOMLパーサーの問題を解決できる
TOMLはPythonのエコシステムにおいても既に特別な位置にあり、標準サポートになることは理になかっている
パーサーはオープンソースとして提供されている tomli を実装の基本として使用している
現在のところ、読み込みだけがサポートされている
読み込みのみのサポートについて補足すると、PEP 680 においてはTOMLの書き込みに関する具体的なサードパーティパッケージの記述はありません。 しかし、tomllibモジュールのドキュメント には以下のパッケージについて引用されています。TOMLの書き込みが必要な場合に導入を検討しましょう。
Pythonのライブラリパッケージングにおいては、インストールに必要な依存関係を pyproject.toml
に定義することになっており、このような背景もありTOMLパーサーの標準ライブラリ化が進んだと思われます。
pyproject.toml
については先述の PEP 517 、 PEP 518 、 PEP 621 の他にも以下のサイトに記載がありますので参考にしてみてください。
以下は pyproject.toml
をビルド設定ファイルとして記述した例です。
build-system
テーブルにビルドに関する情報を定義し、 project
テーブルにメタデータが定義されていることがわかります。
[build-system]
requires = ["setuptools>=40.8.0", "wheel"] # 使用ツール
build-backend = "setuptools.build_meta"
[project]
name = "hello-monthly-topics" # パッケージ名
version = "1.0.0"
description = "This is an example app"
readme = "README.md"
authors = [{ name = "Satoru Kadowaki", email = "mail@example.com" }]
dependencies = [ # 依存パッケージ
"requests >= 2.25.1",
'tomli; python_version < "3.11"',
]
requires-python = ">=3.9"
tomllibの基本的な使い方¶
それでは、ファイルや文字列に記述された簡単なTOMLフォーマットをパースしてみます。 tomllib モジュールはとてもシンプルで、以下の2つの関数が利用できるだけです。
loads()
関数: 文字列で記載されたTOMLをパースして辞書(dict)型を返すload()
関数: TOMLファイルをパースして辞書(dict)型を返す
サンプルコードを実行する際に使用したTOMLは以下のとおりです。
writer = "kadowaki"
month = 11
pi = 3.14
is_example = true
created_at = 2022-11-01T22:38:34+09:00
due_date = 2022-10-30
values = [1, 2, 3]
[table]
name.first = "Tom" # ドット付きキーで同一属性をまとめる(インラインテーブル)
name.last = "Preston-Werner"
birthday = { year = 1994, month = 1 } # 波括弧でまとめたインラインテーブル
[[editors]] # テーブルの配列
name = "kadowaki"
month = 11
short_title = "toml"
[[editors]]
name = "Fukuda"
month = 10
short_title = "async"
example.tomlというファイルに保存された上記のTOMLをパースする場合は以下のように行います。
import tomllib
from pprint import pprint
with open("example.toml", mode="rb") as f:
pprint(tomllib.load(f))
サンプルコードを実行すると、下記の結果が得られます。
指定したキーと値が辞書型で取得され、以下のように自動的にPythonのオブジェクトに変換されていることがわかります。
また、インラインテーブル( table
)はネスト構造化された辞書型に、テーブルの配列( editors
)は辞書型のリストに変換されています。
{'created_at': datetime.datetime(2022, 11, 1, 22, 38, 34, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400))),
'due_date': datetime.date(2022, 10, 30),
'editors': [{'month': 11, 'name': 'kadowaki', 'short_title': 'toml'},
{'month': 10, 'name': 'Fukuda', 'short_title': 'async'}],
'is_example': True,
'month': 11,
'pi': 3.14,
'table': {'birthday': {'month': 1, 'year': 1994},
'name': {'first': 'Tom', 'last': 'Preston-Werner'}},
'values': [1, 2, 3],
'writer': 'kadowaki'}
同様に文字列として読み込む場合は以下のように行います。
import tomllib
from pprint import pprint
toml_doc = """ここにexample.tomlの内容を記述します"""
pprint(tomllib.loads(toml_doc))
先述のとおり、loadまたはloads関数ではTOMLフォーマットを読み込んだ結果を辞書(dict)型として返します。 それぞれの値はPythonの基本ブジェクト(str, int, boolなど)に自動的に変換されますが、その変換テーブルは以下のようになっています。
TOML |
Python |
---|---|
table |
dict |
string |
str |
integer |
int |
float |
float |
boolean |
bool |
オフセットdate-time |
datetime.datetime(Awareオブジェクト) [1] |
ローカルdate-time |
datetime.datetime(Naiveオブジェクト) [1] |
local date |
datetime.date |
local time |
datetime.time |
array |
list |
例外処理¶
TOMLフォーマットをloadまたはloads関数でパースできない場合に TOMLDecodeError
が返されます。
下記のサンプルコードでは、 同一のキー name
が2回定義されています。
TOMLではキーの重複を許容していないためエラーになります。
import tomllib
from pprint import pprint
toml_doc = """
name = "Tom"
name = "Preston-Werner"
"""
pprint(tomllib.loads(toml_doc))
このコードを実行すると、以下のような結果になります。
エラーメッセージとして tomllib.TOMLDecodeError: Cannot overwrite a value (at line 3, column 24)
と出力されている他にも、Python 3.11で改善されたTracebackがより細かいエラー箇所の位置を示しています。
$ python example_error.py
Traceback (most recent call last):
File "/202211-code/example_error.py", line 9, in <module>
pprint(tomllib.loads(toml_doc))
^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/tomllib/_parser.py", line 102, in loads
pos = key_value_rule(src, pos, out, header, parse_float)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/tomllib/_parser.py", line 349, in key_value_rule
raise suffixed_err(src, pos, "Cannot overwrite a value")
tomllib.TOMLDecodeError: Cannot overwrite a value (at line 3, column 24)
書き込みについて¶
先述のとおり、tomllibモジュールでは読み込みのみがサポートされています。 TOMLの書き込みがサポートされない大きな理由として、TOML自体のスタイル仕様でインデントや引用符については揺らぎを許容していることがあげられます。 書き込みをサポートする場合にはインデントをどうするかなどを確定する必要があったことから、書き込みについてはサードパーティに委ねることになりました。
TOMLの書き込みについては先述のとおり tomli-w や tomlkit がありますが、ここでは tomli-w の使用方法について簡単に紹介します。
インストールは pip
コマンドで行います。
$ pip install tomli-w
書き込みは dumps()
関数または dump
関数を使用します。
dumps()
関数: 辞書オブジェクトをTOMLフォーマットに変換して文字列(str)型を返すdump()
関数: 辞書オブジェクトをTOMLフォーマットに変換してファイルオブジェクトに書き込む
以下のサンプルではparamsで定義した辞書型オブジェクトをTOMLフォーマットに変換して、標準出力とファイルに出力しています。
paramsの中身は load()
関数で出力された結果の一部を抜粋してPythonオブジェクトにしています。
import datetime
import tomli_w
params = {
"created_at": datetime.datetime(
2022, 11, 1, 22, 38, 34, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400))
),
"editors": [
{"month": 11, "name": "kadowaki", "short_title": "toml"},
],
"table": {
"birthday": {"month": 1, "year": 1994},
"dob": datetime.datetime(1979, 5, 27, 7, 32),
"name": {"first": "Tom", "last": "Preston-Werner"},
},
}
print(tomli_w.dumps(params)) # 文字列として出力
with open("./example2.toml", "wb") as f: # ファイルに出力
tomli_w.dump(params, f)
出力結果は以下のようになります。
[table]
の出力結果が元データとは少し異なっていることからも、書き込みモジュールによって揺らぎが起こることは容易に想像できそうです。
created_at = 2022-11-01 22:38:34+09:00
editors = [
{ month = 11, name = "kadowaki", short_title = "toml" },
]
[table]
dob = 1979-05-27 07:32:00
[table.birthday]
month = 1
year = 1994
[table.name]
first = "Tom"
last = "Preston-Werner"
TOMLフォーマットのバリデーション¶
現時点ではTOMLフォーマットに関する明確な型チェックはサポートされていません。 しかしながら、以前の記事 Python最新バージョン対応!より良い型ヒントの書き方 でも紹介したTypedDictや、サードパーティライブラリである pydantic を利用して読み込まれたTOMLにバリデーションを行うことができます。 本記事では上記の紹介のみとなりますが、興味のある方はTypedDictやPydanticを試してみてください。
まとめ¶
Pythonスクリプトを実行するために必要な設定ファイルのフォーマットにJSONやYAMLがよく使用されますが、TOMLはこれらと比較しても簡潔に柔軟なデータ構造を表現できるデータフォーマットです。 複雑なデータ構造の読み込みを追加モジュールなしに利用でき、Pythonオブジェクトに自動変換されることは開発者にとってとてもありがたいことです。 設定ファイルをどのフォーマットにするか悩まされてきたみなさんも、今後はTOMLフォーマットを中心に考えると良さそうです。 Python 3.11に移行する際には是非利用してみてください!