筒井@ryu22eです。2023年最初の
PEP 681についての公式ドキュメントは以下を参照してください。
PEP 681 – Data Class Transforms | peps.
Pythonには、データクラスと似た構造を持つクラスを扱うライブラリがいくつかあります。たとえば、attrs、pydantic、O/
以下ではO/dataclass_
デコレーターが力を発揮する場面を説明します。
PythonのO/Rマッパーを使っていて困ること
ここでは、PythonのO/
サンプルコードで使用するO/orm.
としてカレントディレクトリに置いて使う前提とします。
また、型チェッカーは現時点
O/Rマッパーでは初期化処理の型チェックができない
O/orm.
を使って書籍を表すBook
クラスを定義しています。
Book
クラスはPythonのデータクラスによく似ていますが別物です。データクラスと違って属性の型に関する情報がありません。books.
の中には、以下のようなBook
クラスを初期化するコードが書いてあります。
book = Book(
title="Python実践レシピ",
# priceは整数型なのでこれは間違っている
price="定価2,970円(本体2,700円+税10%)",
)
上記のコードはprice
に指定した値の型が明らかに間違っていますが、型チェッカーBook
クラスを初期化する際に呼ばれるBook.__
メソッドの引数に型情報がないためです。Pyrightで型チェックを実行してみましょう。以下のようにエラーメッセージは発生しません。
$ pyright books.py (省略) 0 errors, 0 warnings, 0 informations Completed in 0.512sec ✨ Done in 0.86s.
Book.__
メソッドの定義も見ておきましょう。以下のように、引数が**kwargs
となっており、型情報がないことがわかります。
>>> from books import Book
Baseクラスの初期化処理
>>> help(Book.__init__)
Help on function __init__ in module orm:
__init__(self, **kwargs)
Initialize self. See help(type(self)) for accurate signature.
(END)
今回はO/orm.
を使用しましたが、SQLAlchemy、Djangoを使ってもBook.__
メソッドの定義は型情報がありません。
一方、データクラスの初期化処理では型チェックができる
一方、データクラスで同様のコードを書いた場合はどうなるのか見てみましょう。
データクラスはクラスに定義した型アノテーションを元に、dataclasses.
前述のBook
クラスに近い構造のデータクラスを以下のように定義しました。
データクラスで定義したBook
クラスの__
メソッドには型情報があるので、Pyrightで型チェックを実行するとエラーが出力されます。
$ pyright dataclass_books.py (省略) /***/dataclass_books.py /***/dataclass_books.py:11:11 - error: Argument of type "Literal['定価2,970円(本体2,700円+税10%)']" cannot be assigned to parameter "price" of type "int" in function "__init__" "Literal['定価2,970円(本体2,700円+税10%)']" is incompatible with "int" (reportGeneralTypeIssues) 1 error, 0 warnings, 0 informations Completed in 0.448sec error Command failed with exit code 1. info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
こちらもBook.__
メソッドの定義を確認してみましょう。books.
に定義したBook
クラスとは異なり、title
にはstr
、price
にはint
の型情報を持っていることがわかります。
>>> from dataclass_books import Book
>>> help(Book.__init__)
Help on function __init__ in module dataclass_books:
__init__(self, title: str, price: int) -> None
Initialize self. See help(type(self)) for accurate signature.
O/Rマッパーのクラスをデータクラス化するとどうなるか
データクラスなら初期化処理の型チェックができるというなら、O/books.
のBook
クラスに定義したフィールドtitle
、price
を型アノテーションにし、dataclasses.
デコレーターも付けました。
上記のBook
クラスはデータクラスなので、型チェックを行うことはできます。
$ pyright books2.py (省略) /***/books2.py /***/books2.py:13:11 - error: Argument of type "Literal['定価2,970円(本体2,700円+税10%)']" cannot be assigned to parameter "price" of type "int" in function "__init__" "Literal['定価2,970円(本体2,700円+税10%)']" is incompatible with "int" (reportGeneralTypeIssues) 1 error, 0 warnings, 0 informations Completed in 0.454sec error Command failed with exit code 1.
しかし、基底クラスBase
の__
メソッドを呼ぶことができなくなります。まず、正しい挙動を確認するためにbooks.
を実行します。以下のようにBase
クラスの__
メソッドに書いたprint
関数が呼ばれ、Baseクラスの初期化処理
が表示されます。
$ python books.py Baseクラスの初期化処理
次に、books2.
を実行します。今度はBaseクラスの初期化処理
が表示されません。dataclasses.
デコレーターによって、型情報付きのBook.__
メソッドが自動生成されるためです。
$ python books2.py # Base.__init__メソッドの処理が呼ばれない
以下のようにdataclasses.
デコレーターを使わず型アノテーションだけ定義した場合も考えてみましょう。
今度は基底クラスBase
の__
メソッドを呼び出せます。
$ python books3.py Baseクラスの初期化処理
しかし、型情報付きのBook.__
が作られないので、型チェックを行うことができません。
$ pyright books3.py (省略) 0 errors, 0 warnings, 0 informations Completed in 0.446sec ✨ Done in 0.80s.
どうやら、O/
typingモジュールの dataclass_transform
デコレーターならO/Rマッパーのクラスをデータクラスのように扱える
この状況を改善してくれるのが、typingモジュールのdataclass_
デコレーターです。dataclass_
デコレーターは、データクラスではないクラスにデータクラスで行っている型チェックの一部を導入する機能です。dataclass_
デコレーターの使い方はいくつかありますが、今回はクラスデコレーターとして機能する独自の関数を定義して組み合わせる方法を紹介します[3]。
まず、my_
をカレントディレクトリに作成し、中にBook
クラスに適用するデコレーターcreate_
を定義します。create_
デコレーターにはdataclass_
デコレーターを使います。また、内部の処理ではクラスの型アノテーションを元にフィールドを追加する処理を書いておきます。実際のO/str
、int
以外の型や初期値を指定した場合などの対応が必要なので、もっと複雑なコードになりますが、今回はBook
クラスの定義に必要なコードのみを書いています。
次に、前述のbooks3.
のBook
モデルにcreate_
デコレーターを使うようにします。
このコードに対してPyrightで型チェックを実行すると、データクラスのように型情報付きのBook.__
があるものとして扱ってくれます。
Pyrightの実行結果を以下に載せます。price
に指定した型が間違っているので、エラーを検出してくれています。
$ pyright books4.py (省略) /***/books4.py /***/books4.py:12:11 - error: Argument of type "Literal['定価2,970円(本体2,700円+税10%)']" cannot be assigned to parameter "price" of type "int" in function "__init__" "Literal['定価2,970円(本体2,700円+税10%)']" is incompatible with "int" (reportGeneralTypeIssues) 1 error, 0 warnings, 0 informations Completed in 0.452sec error Command failed with exit code 1. info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
また、Book
クラスはデータクラスではないので、実行時に基底クラスBase
の__
メソッドを呼び出せます。
$ python books4.py Baseクラスの初期化処理
dataclass_transform
デコレーターの内部では何をやっているのか
dataclass_
デコレーターの実装はとてもシンプルです。CPython 3.
def dataclass_transform(
*,
eq_default: bool = True,
order_default: bool = False,
kw_only_default: bool = False,
field_specifiers: tuple[type[Any] | Callable[..., Any], ...] = (),
**kwargs: Any,
) -> Callable[[T], T]:
def decorator(cls_or_fn):
cls_or_fn.__dataclass_transform__ = {
"eq_default": eq_default,
"order_default": order_default,
"kw_only_default": kw_only_default,
"field_specifiers": field_specifiers,
"kwargs": kwargs,
}
return cls_or_fn
return decorator
dataclass_
デコレーターを使ったクラスは__
属性が追加されます。これ自体は実行時に何かに使われるものではありません。あくまで型チェッカーに渡すための情報です。型チェッカーは、__
属性があるクラスはデータクラスのように型アノテーションの情報を元にした型チェックを行ってくれます。たとえば、前述のbooks4.
であれば、型チェッカーはtitle
の型をstr
、price
の型をint
として型チェックを行います。
主要な型チェッカーのPEP 681への対応状況
前述したとおり、dataclass_
デコレーターはクラスに型チェッカー用の属性を追加する機能しかありません。つまり、型チェッカーがPEP 681に対応していないと、まったく意味がありません。以下で主要な型チェッカーのPEP 681への対応状況を紹介します。
型チェッカー名 | 2022年12月15日 時点の 最新バージョン |
PEP 681への対応状況 |
---|---|---|
Pyright | 1. |
対応済み |
Mypy | 0. |
未対応 |
Pyre | 0. |
0. |
pytype | 2022. |
未対応 |
「データクラスと似た構造を持つクラスを扱うライブラリ」のPEP 681への対応状況
冒頭で紹介した
ライブラリ名 | 2022年12月15日 時点の 最新バージョン |
PEP 681への対応状況 |
---|---|---|
attrs | 22. |
対応済み。attr. デコレーター、attr. デコレーターがdataclass_ デコレーターと同じ機能を持つ |
pydantic | 1. |
対応済み。pydantic. モデルがdataclass_ デコレーターと同じ機能を持つ |
SQLAlchemy | 1. |
対応済み。attrsを使ったクラスをSQLAlchemy用のクラスにする機能を使うとdataclass_ デコレーターと同じことができる。また、データクラスそのものも利用できる。詳細はIntegration with dataclasses and attrsを参照 |
Django内蔵のO/ |
4. |
未対応。Django Issues、Django Enhancement Proposals |
まとめ
dataclass_
デコレーターの登場によって、より型ヒントを活用できる場面が増えてきそうです。まだ対応していない型チェッカーがあるので、今後に期待ですね!