型チェックを強化するPython 3.11の新機能Data Class Transforms(PEP 681)
This work is licensed under a Creative Commons Attribution 4.0 International License .
はじめに
自己紹介
-
Ryuji Tsutsui @ryu22e
-
株式会社hokan所属
( 今年は Goldスポンサーと して ブースも 出しています) -
Python歴は
12年くらい (主に Django) -
Python Boot Camp、
Shonan.py、 GCPUG Shonanなど コミュニティ活動も しています -
著書
(共著) :『 Python実践レシピ 』
自己紹介
「Python Boot Campの
自己紹介
10/29の
夜には
今日話したいこと
-
Python 3.11の
新機能Data Class Transforms (PEP 681)に ついて 解説
この発表を聞いて何を得られるか
-
Data Class Transforms
(PEP 681) 登場以前に Pythonに 存在した 問題を 理解できる -
Data Class Transforms
(PEP 681)に より どう やって 前述の 問題を 解決するのか理解できる -
上記2つを
知る ことで、 Python 3.11以降が より 堅牢な コードを 書ける ことを 理解できる
PEP 681を一言で説明すると
PEP 681を一言で説明すると
「データクラス風ライブラリ」の
…だけじゃ分かりませんよね?
「データクラス風ライブラリ」は
そもそもデータクラスとは
クラスにdataclasses.dataclass
デコレーターに
from dataclasses import dataclass
@dataclass
class Book:
# ↓型アノテーション
title: str
price: int
book = Book(title="Python実践レシピ", price=2970)
print(book.title, book.price)
# price引数の型が間違っているので型チェッカーではエラーになる
book = Book(
title="Python実践レシピ",
price="定価2,970円(本体2,700円+税10%)",
)
print(book.title, book.price)
データクラスは型チェッカーを使って型チェックできる
「データクラス風ライブラリ」とは
データクラスではないけど、
-
Django 内蔵の
O/Rマッパー
Djangoの例
DjangoでBook
を
from django.db import models
class Book(models.Model):
title = models.CharField(max_length=255)
price = models.IntegerField()
# 使用例
book = Book(title="Python実践レシピ", price=2970)
book.save()
PEP 681以前に存在したある問題
「データクラス風ライブラリ」の
この発表で使う簡易O/Rマッパー
本物の
"""orm.py"""
class Base:
"""リレーショナルデータベースとマッピングさせるクラスの基底クラス"""
def __init__(self, **kwargs):
# 具体的な処理内容は省略
print("Baseクラスの初期化処理")
class String:
"""文字列フィールド用のクラス"""
pass
class Integer:
"""整数フィールド用のクラス"""
pass
この発表で使う簡易O/Rマッパー
"""使用例"""
from orm import Base, String, Integer
class Book(Base):
"""書籍を表すクラス"""
title = String()
price = Integer()
こんなコードを書くとどうなる?
最後に
book = Book(
title="Python実践レシピ",
# priceは整数型なのでこれは間違っている
price="定価2,970円(本体2,700円+税10%)",
)
型チェックではエラーにならない
なぜエラーにならないのか
Book.__init__
には
>>> 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)
データクラスなら型チェックができるが…
from dataclasses import dataclass
@dataclass
class Book:
title: str
price: int
book = Book(
title="Python実践レシピ",
# priceは整数型なのでこれは間違っている
price="定価2,970円(本体2,700円+税10%)",
)
データクラスなら型チェックができるが…
O/Rマッパーとデータクラスの機能のいいとこ取りができないか?
ではこんな風に書けばいいのでは?
from dataclasses import dataclass
from orm import Base
# Baseクラスを継承したデータクラスを作る
@dataclass
class Book(Base):
# Baseクラスで型アノテーションを元にフィールドを作る想定
title: str
price: int
book = Book(
title="Python実践レシピ",
# priceは整数型なのでこれは間違っている
price="定価2,970円(本体2,700円+税10%)",
)
一応型チェックはできる
dataclass
デコレーターが型ヒントを作ってくれるので、型チェックができる
>>> from books import Book
Baseクラスの初期化処理
>>> help(Book.__init__)
Help on function __init__ in module orm:
__init__(self, title: str, price: int) -> None
Initialize self. See help(type(self)) for accurate signature.
Base.__init__
に定義されたコードが呼ばれなくなった
Base.__init__
に
class Base:
"""リレーショナルデータベースとマッピングさせるクラスの基底クラス"""
def __init__(self, **kwargs):
# 具体的な処理内容は省略
print("Baseクラスの初期化処理") # ←これが呼ばれない
$ python books2.py # "Baseクラスの初期化処理"が表示されない
なぜ Base.__init__
が呼ばれないのか
Base.__init__
がdataclass
デコレーターは__init__
を
ライブラリによっては型ヒントの恩恵を受けるのは難しい場合もある
型チェッカー側で
Mypyのプラグイン機能
例えば
# 設定ファイル(mypy.ini)にこんな形でプラグインを指定できる
[mypy]
plugins = /one/plugin.py, other.plugin
参考: https://mypy.readthedocs.io/en/stable/extending_mypy.html#configuring-mypy-to-use-plugins
プラグインにも問題がある
ただし、
PEP 681登場によって何が解決されるのか
typingモジュールに
dataclass_transformデコレーターの使用例
時間の
-
自作の
関数デコレーターに 使う 方法 -
自作の
基底クラスに 使う 方法 -
自作の
メタクラスに 使う 方法
dataclass_transformデコレーターの使用例
まず、my_orm.py
を
from typing import TypeVar, dataclass_transform
from orm import Integer, String
T = TypeVar("T")
@dataclass_transform()
def create_model(cls: type[T]) -> type[T]:
# クラスの型アノテーションを元にフィールドを追加
for key, value in cls.__annotations__.items():
if value is str:
setattr(cls, key, String())
elif value is int:
setattr(cls, key, Integer())
return cls
dataclass_transformデコレーターの使用例
次に、
from my_orm import create_model
from orm import Base
@create_model
class Book(Base):
title: str
price: int
book = Book(
title="Python実践レシピ",
# priceは整数型なのでこれは間違っている
price="定価2,970円(本体2,700円+税10%)",
)
型チェックしてみると…
データクラスと
dataclass_transformデコレーターの仕組みについて解説
dataclass_transformデコレーターのソースコードはこうなっている
dataclass_transform
デコレーターは__dataclass_transform__
属性を
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_transformデコレーターのソースコードはこうなっている
型チェッカーは__dataclass_transform__
属性を
型チェッカーのPEP 681への対応状況
以下に
-
Pyright(1.1.328)
-
Mypy(1.6.1)
-
Pyre(0.9.18)
-
pytype(2023.9.27)
調べた結果
2023年10月27日現在、
Pyrightについて
以下
Mypyについて
この
Pyreについて
0.9.11の
pytypeについて
Python 3.11対応自体が
PyrightはVS Codeから簡単に呼び出せる
Pylanceと
PyrightはVS Codeから簡単に呼び出せる
「データクラス風ライブラリ」のPEP 681への対応状況
以下に
-
attrs(23.1.0)
-
Pydantic(2.4.2)
-
SQLAlchemy(2.0.21)
-
Django内蔵の
O/Rマッパー(4.2.5)
調べた結果
Django以外は
attrsについて
attr.define
デコレーターがdataclass_transform
デコレーターに
import attr
@attr.define
class Book:
title: str
price: int
Pydanticについて
pydantic.BaseModel
クラスがdataclass_transform
デコレーターに
from pydantic import BaseModel
class Book(BaseModel):
title: str
price: int
SQLAlchemyについて
dataclass_transform
デコレーターに
1つ目はsqlalchemy.orm.MappedAsDataclass
クラス。
from sqlalchemy.orm import (DeclarativeBase, Mapped, MappedAsDataclass,
mapped_column)
class Base(DeclarativeBase):
pass
class Book(MappedAsDataclass, Base):
__tablename__ = "book"
id: Mapped[int] = mapped_column(init=False, primary_key=True)
title: Mapped[str]
price: Mapped[int]
SQLAlchemyについて
2つ目はregistry.mapped_as_dataclass()
デコレーター。
from sqlalchemy.orm import Mapped, mapped_column, registry
reg = registry()
@reg.mapped_as_dataclass(unsafe_hash=True)
class Book:
__tablename__ = "book"
id: Mapped[int] = mapped_column(init=False, primary_key=True)
title: Mapped[str]
price: Mapped[int]
SQLAlchemyについて
また、
import attr
from sqlalchemy import Column, Integer, String, Table
from sqlalchemy.orm import Mapped, registry
mapper_registry = registry()
@attr.define(slots=False)
class Book:
id: Mapped[int] = attr.ib(init=False)
title: Mapped[str]
price: Mapped[int]
# ↓まだ続きがある
SQLAlchemyについて
型アノテーションと
# ↑前の続き
book = Table(
"book",
mapper_registry.metadata,
Column("id", Integer, autoincrement=True, primary_key=True),
Column("title", String(50)),
Column("price", Integer),
)
mapper_registry.map_imperatively(Book, book)
Django内蔵のO/Rマッパーについて
Issue Tracker と
まとめ
まとめ1
-
PEP 681登場以前、
「データクラス風ライブラリ」では、 初期化処理に 関する 型チェックを 行う ことができなかった -
PEP 681で
これらの ライブラリでも データクラスのような 型チェックを できる
まとめ2
-
2023年10月27日現在、
PEP 681対応を 謳っている 型チェッカーは Pyrightのみ。 他の 型チェッカーがんばれ! -
attrs、
Pydantic、 SQLAlchemyは PEP 681に 対応している。 Djangoも 対応して ほしい 😢