dataclassを捨ててpydanticに乗り換える
Pydanticが今最高にCool
こんにちは、極論モンスターのYosematです。pydanticに替えてdataclassを使う理由は今ほとんどありません。pydanticがV2になったこのタイミングでpydanticに乗り換えましょう。この記事ではなぜdataclassよりもpydanticなのか理由を述べていきます。
※2024/02/26追記 OpenAIのクライアントもPydanticを採用しました
素敵なブログからの引用。ただし現在はdataclassもslotを導入している。slotを利用して通常より高速にフィールドアクセスしたい人はattrsやdataclassもアリ。
理由① より洗練されたインターフェース
pydanticをdataclassに代えて使うのはなんといってもかゆいところに手が届くインターフェースです。はっきりいってdataclassも素晴らしいライブラリでちょっとした工夫でたいていの差は埋められますが、何度も何度も使う基本的な機能だけにimport
文1つの差でも大きなユーザビリティの差として感じられます。
Serialize (as dict or json)
Pydanticの場合
pydanticはSerialize用の機能がモデルに備わっていてメソッドとして使う。datetime型の自動変換などのかゆいところに手が届く。
from datetime import datetime
from pydantic import BaseModel
class Product(BaseModel):
name: str
price: int
class Delivery(BaseModel):
timestamp: datetime
products: list[Product]
deliv = Delivery(
timestamp=datetime.fromisoformat("2020-01-02T03:04:05Z"),
products=[Product(name="sushi", price=1000)],
)
deliv_dict = deliv.model_dump() # ◎
# >> {'timestamp': datetime.datetime(2020, 1, 2, 3, 4, 5, tzinfo=datetime.timezone.utc), 'products': [{'name': 'sushi', 'price': 1000}]}
deliv_json = deliv.model_dump_json() # ◎
# >> {"timestamp":"2020-01-02T03:04:05Z","products":[{"name":"sushi","price":1000}]}
dataclassの場合
dataclassはSerialize用の機能がモジュールに備わっていてimportして使う。json化するときはdictを経由しjsonライブラリをimportする。
import json
from dataclasses import asdict, dataclass
from datetime import datetime
@dataclass
class Product:
name: str
price: int
@dataclass
class Delivery:
timestamp: datetime
products: list[Product]
deliv = Delivery(
timestamp=datetime.fromisoformat("2020-01-02T03:04:05Z"),
products=[Product(name="sushi", price=1000)],
)
deliv_dict = asdict(deliv) # ◎
# >> {'timestamp': datetime.datetime(2020, 1, 2, 3, 4, 5, tzinfo=datetime.timezone.utc), 'products': [{'name': 'sushi', 'price': 1000}]}
deliv_json = json.dumps(asdict(deliv)) # ×
# >> Object of type datetime is not JSON serializable
Deserialize (from dict or json)
pydanticの場合
pydanticはネストされたdictやjson形式のstrからモデルインスタンスを生成できる。
変数を1つずつ渡す通常のモデル初期化に加えいちいち展開しなくても良いmodel_validate
やjson形式のstrから直接生成できるmodel_validate_json
をclassmethodとして備えている。
__init__
を直接呼ばないclassmethodを使ったインスタンス生成はRustなどのモダンで硬い言語でも採用されているCoolな方法。
deliv_dict = {
"timestamp": datetime(2020, 1, 2, 3, 4, 5, tzinfo=timezone.utc),
"products": [{"name": "sushi", "price": 1000}],
}
deliv_json = (
'{"timestamp":"2020-01-02T03:04:05Z","products":[{"name":"sushi","price":1000}]}'
)
deliv_from_dict = Delivery(**deliv_dict)
deliv_from_dict = Delivery.model_validate(deliv_dict)
deliv_from_json = Delivery.model_validate_json(deliv_json)
dataclassの場合
dataclassの場合ネストされたdictからインスタンスを一発で生成することはできないのでネストされた要素を1つ1つモデルに変換していく。jsonはdictを経由しなければならないしdatetimeは自前で変換しなければならない。
deliv_dict = {
"timestamp": datetime(2020, 1, 2, 3, 4, 5, tzinfo=timezone.utc),
"products": [{"name": "sushi", "price": 1000}],
}
deliv_json = (
'{"timestamp":"2020-01-02T03:04:05Z","products":[{"name":"sushi","price":1000}]}'
)
deliv_from_dict = Delivery(
timestamp=deliv_dict["timestamp"],
products=[
Product(name=p["name"], price=p["price"]) for p in deliv_dict["products"]
],
)
deliv_dict_from_json = json.loads(deliv_json)
deliv_from_json = Delivery(
timestamp=datetime.fromisoformat(deliv_dict_from_json["timestamp"]),
products=[
Product(name=p["name"], price=p["price"])
for p in deliv_dict_from_json["products"]
],
)
その他のPydanticの魅力的なインターフェースたち
dictからだけじゃない、スゴいインスタンス生成
pydanticはmodel_config
という特殊なモデルの設定変数がある。これにfrom_attributes
引数を1つ通してやるだけで、どんな型のオブジェクトからでもPydanticモデルに変換することができる。使うのはいつもどおりのmodel_validate
from dataclasses import dataclass
from pydantic import BaseModel, ConfigDict
class Product(BaseModel):
model_config = ConfigDict(from_attributes=True)
name: str
price: int
@dataclass
class ProductDC:
name: str
price: int
product_dc = ProductDC(name="sushi", price=1000)
product_pd = Product.model_validate(product_dc)
Shallow/Deep Copy
copyモジュールからimportしなくてもメソッドとしてmodel_copyが生えている。
from pydantic import BaseModel
class Product(BaseModel):
name: str
price: int
product = Product(name="sushi", price=1000)
product_deepcopy = product.model_copy(deep=True)
理由② Run-time Validation
そしてもちろんRun-time Validationが効くのもdataclassにはない魅力です。一般的にはこちらがメインで紹介されるpydanticの魅力。
pydanticの場合
int型の変数にstrを入れたりするとエラーになります。
from pydantic import BaseModel
class Product(BaseModel):
name: str
price: int
def create_product(prod_duct: dict) -> Product:
return Product(**prod_duct)
prod_deliv = {"name": "sushi", "price": "invalid_value"}
prod = create_product(prod_deliv) # Raises ValidationError
dataclassの場合
dataclassを使ってしまうとint型にstr型の値を代入してもエラーが出ません。
静的解析は強力ですが完璧ではありません。下の例では静的解析をすり抜けて不適切な値がpriceに代入されてしまっています。
from dataclasses import dataclass
@dataclass
class Product:
name: str
price: int
def create_product(prod_duct: dict) -> Product:
return Product(**prod_duct)
prod_deliv = {"name": "sushi", "price": "invalid_value"}
prod = create_product(prod_deliv) # Runs without error
さらに発展的なバリデーション
pydanticの動的解析では更に発展的なバリデーションも行えます。
field_validatorなどの素敵な機能もあるのですが、ここでは私のおすすめのAnnotatedを使ったバリデーションをご紹介。
Annotated[T, AfterValidator(valid_func)]
以下の例では値が整数の二乗であるかをcheck_squares
関数によってチェックします。このように関数をAnnotated
とAfterValidator
を使って登録することで型レベルだけでなく値レベルでカスタムロジックでバリデーションできるようになるのはdataclassにはない最高の機能だと思います。
from typing import Annotated
from pydantic import BaseModel
from pydantic.functional_validators import AfterValidator
def check_squares(v: int) -> int:
assert v**0.5 % 1 == 0, f"{v} is not a square number"
return v
SquaredNumber = Annotated[int, AfterValidator(check_squares)]
class DemoModel(BaseModel):
square_numbers: list[SquaredNumber] = []
valid_model = DemoModel(square_numbers=[1, 4, 9])
invalid_model = DemoModel(square_numbers=[1, 4, 13]) # Raises ValidationError
その他の詳しい機能について詳しくはvalidatorを参照!
まとめ
以上踏まえるとdataclassを使う理由はありません(極論)!
バージョンがV2になり、FastAPIにも導入されたこのタイミングでpydanticに乗り換えましょう。Yosematでした。
Discussion