門脇
本題の前に、ご存知の方も多いと思いますが、10月24日、ついにPython 3.
先月のPython Monthly Topicsでも紹介した非同期I/
今回ご紹介するtomllibモジュールも新機能の1つですが、すでに馴染みがある人も、今まであまり使ったことがない人も、この機会に目をとおしてみてください。
TOMLとは
TOML
TOMLの主な仕様は以下の通りです。
- 構成要素は、キー
(Key) と値(Value) の組からなり、キーと値は等号(=) で区切られる - 値は、文字列、整数、浮動少数点数、ブーリアン、日時、配列もしくはインライン・
テーブルなどのデータ型を扱うことができる - null
(または nil) タイプは存在しない
- null
- 大文字と小文字を区別
(ケース・ センシティブ) するテキストファイルである - 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.
PEP 680の主な内容を簡単にまとめると、以下のような記載があります。
- TOMLはPythonのパッケージングに選ばれているファイルフォーマットであり、 下記のPEPで提案され実装されている
- TOMLがPythonで標準サポートされることにより、ベンダー依存のあるTOMLパーサーの問題を解決できる
- TOMLはPythonのエコシステムにおいても既に特別な位置にあり、標準サポートになることは理になかっている
- パーサーはオープンソースとして提供されているtomliを実装の基本として使用している
- 現在のところ、読み込みだけがサポートされている
読み込みのみのサポートについて補足すると、PEP 680においてはTOMLの書き込みに関する具体的なサードパーティパッケージの記述はありません。しかし、tomllibモジュールのドキュメントには以下のパッケージについて引用されています。TOMLの書き込みが必要な場合に導入を検討しましょう。
Pythonのライブラリパッケージングにおいては、インストールに必要な依存関係をpyproject.
に定義することになっており、このような背景もあり、TOMLパーサーの標準ライブラリ化が進んだと思われます。
pyproject.
については先述のPEP 517、PEP 518 、PEP 621の他にも以下のサイトに記載がありますので参考にしてみてください。
以下はpyproject.
をビルド設定ファイルとして記述した例です。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 = "[email protected]" }]
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.
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フォーマットを読み込んだ結果を辞書
TOML | Python |
---|---|
table | dict |
string | str |
integer | int |
float | float |
boolean | bool |
オフセットdate-time | datetime. |
ローカルdate-time | datetime. |
local date | datetime. |
local time | datetime. |
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.
と出力されている他にも、Python 3.
$ 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)
TOMLの書き込みについて
先述のとおり、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 Monthly Topics 2022年9月の記事
本記事では上記の紹介のみとなりますが、興味のある方はTypedDictやpydanticを試してみてください。
まとめ
Pythonスクリプトを実行するために必要な設定ファイルのフォーマットにJSONやYAMLがよく使用されますが、TOMLはこれらと比較しても簡潔に柔軟なデータ構造を表現できるデータフォーマットです。複雑なデータ構造の読み込みを追加モジュールなしに利用でき、Pythonオブジェクトに自動変換されることは開発者にとってとてもありがたいことです。
設定ファイルをどのフォーマットにするか悩まされてきたみなさんも、今後はTOMLフォーマットを中心に考えると良さそうです。Python 3.