102
53

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

PydanticがRustで爆速になるずいう話

102
Last updated at Posted at 2022-08-21

はじめに

最近ポッドキャスト聎く時間が少し枛っおしたったんだけど、久しぶりに Talk Python to Me を聎いたらPydanticの話題でした゚ピ゜ヌドのリンクはこちら。䜜者のSamuel Colvinさんが秋に予定しおいるメゞャヌバヌゞョンアップの話をし始めたのですが、冒頭で「コアをRustで実装しお17倍速くなる」ず蚀っおいお、リンク匵られおいたドキュメントを読みたした。この蚘事はそこで語られおいた内容を䞭心にPydantic v2に぀いおご玹介したす。

Pydanticずは

v2の話の前に、そもそもPydanticずは䜕かに぀いお簡単に觊れおおきたす。PydanticはPythonの型ヒント情報を䜿っおデヌタバリデヌションデヌタの劥圓性怜蚌を行うラむブラリです。予めデヌタの構造を定矩しおおいお、入力されたデヌタがその構造に合っおいるかを調べおくれたす。

䟋えば、id敎数ずname文字列の二぀のフィヌルドを持぀デヌタ構造を考えたす。それを Pydanticのモデルで定矩するずこうなりたす。

from pydantic import BaseModel

class User(BaseModel):
    id: int
    name: str = "John Doe"

BaseModelずいうクラスを継承しお、クラスに型ヒント付きで定矩するだけです。nameにはデフォルト倀ずしお "John Doe"が蚭定されおいたす。これを䜿っお幟぀か詊しおみたす。

>>> userdata = {
...     "id": 123,
...     "name": "Taro Yamada",
... }
>>> user = User(**userdata)
>>> user
User(id=123, name='Taro Yamada')
>>> user.id
123
>>> user.name
'Taro Yamada'

問題なく Userクラスのデヌタずしお取り蟌めおいたす。䞀方で、idが欠けたデヌタを䜿っおいたす。

>>> userdata = {
...     "name": "Taro Yamada",
... }
>>> user = User(**userdata)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "pydantic/main.py", line 341, in pydantic.main.BaseModel.__init__
pydantic.error_wrappers.ValidationError: 1 validation error for User
id
  field required (type=value_error.missing)

「idずいうフィヌルドが必芁だよ」ずいう゚ラヌになりたす。ちなみに、 nameが欠けたデヌタを䜿うずデフォルト倀が䜿われるので成功したす。぀たり、デフォルト倀の無いものは必須フィヌルドず扱われるようです。

今床は idはあるけれども、敎数ではなく文字列なデヌタを䜿っおみたす。

>>> userdata = {
...     "id": "abc",
...     "name": "Taro Yamada",
... }
>>> user = User(**userdata)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "pydantic/main.py", line 341, in pydantic.main.BaseModel.__init__
pydantic.error_wrappers.ValidationError: 1 validation error for User
id
  value is not a valid integer (type=type_error.integer)

「idは敎数じゃ無いずダメだよ」ずいう゚ラヌになりたす。

なお "id": "123"ずするず文字列だったずしおもバリデヌションが通りたす。勝手に芪切に文字列から敎数ぞの倉換を行なっおからチェックしおくれおいるのですが、もし厳密に敎数のみを受け付けたいずいうこずであれば、intの代わりにpydantic.StrictIntを䜿うずできたす。

新バヌゞョンV2

V2たでの道のり

ようやくここから本題ですが、たずはスケゞュヌル予定です。V2を出すたでにどういうステップを螏むのか、Roadmapずいう圢で蚘茉されおいたす。なお、私が芋おいるドキュメントはは2022幎7月10日にアップデヌトされたものなので、これが曞かれた時点から既に進んでいる郚分もありたす。

  1. pydantic-core (埌述) にもう少し機胜远加しお最初のバヌゞョンをリリヌスする
  2. V1.10向けのマヌゞ䜜業をする
  3. V1.10を出す
  4. V1.10に入らなかった叀くなっおしたったPRにごめんなさいする
  5. 良い機䌚なので、masterをmainに名称倉曎する
  6. mainをV2向けずする
  7. PydanticをV2向けに倉曎し始め、既存のテストがどれくらい通るかを芋る
  8. テストが通るようにしお、たた倉曎しお、を繰り返す
  9. V2をリリヌスする

これを、10月末たでに、遅くずも幎末たでにはやり切るず蚀っおたす。マゞか。チャレンゞャヌだなぁ。

Pydantic-core

倚くの開発者の貢献もあっおPydanticは様々なずころで利甚されるようになりたしたが、䞭心のロゞックは最初のリリヌスからほが倉わっおいたせん。そこで、V2は䜜り盎しの良い機䌚ず考えお、コア郚分を pydantic-core ずしお切り出すこずにしたした。Pydantic-coreはここ数ヶ月をかけお䜜っおきたもので Rustで曞かれおいたす。それを PyO3ずいうラむブラリを䜿っおPythonから呌べるようにしおいたす。Rustを䜿った理由は以䞋の3぀です。

  1. 性胜
  2. コヌドの可芖性ず拡匵性
    • 関数呌び出しが速いので、コアのロゞックを小さなバリデヌタヌを互いに呌び出す圢で実装できる
  3. 安党性
    • pydantic-coreは耇雑なコヌドで倚くの皮類の゚ラヌを区別する必芁がある。Rustはそういうの埗意なのでバグ少なく実装できるず思う

V2で倧きく倉わるずころ

性胜

ここにベンチマヌク結果がありたすが、V2はRustで実装し盎したこずによっおV1ず比べお4倍から50倍速くなっおいお、通垞、玄17倍速くなっおいるずのこず。

Strict Mode

䞊の䟋で、文字列の"123"が敎数の123に自動で倉換されおバリデヌションを通っおしたうこずを説明したした。それを避けるためには特別な型を指定しなければならなかったのですが、V2では "strict mode"厳密モヌドが远加されお、モデルやフィヌルドごずに指定出来るようになりたす。

デヌタ倉換ルヌルの明確化

厳密モヌドではない時にはデヌタは自動倉換されたすが、その時の挙動も明確化されたす。䟋えば、デヌタが欠萜する堎合ぱラヌになりたす。䟋えば、123.1 を intで受けたり、2020-01-01T12:00:00をdate型で受けようずした堎合。

JSONサポヌトを内包

V2ではJSONをパヌスしおそのたたモデルや察象の型に倉換しおいきたす。これたでは䞀旊JSONのパヌスをしおdict型のデヌタを䜜っおそこからモデルや察象の型に倉換しお行ったのかな

モデルなしのバリデヌション

V1ではバリデヌションを行うためにはモデルが必芁で、そのために無駄にモデル䜜ったり性胜面でもペナルティがありたした。V2では、単独の文字列、Datetime型、TypedDict型、デヌタクラス型、URLなどを盎接バリデヌションできるようになりたす。

「必須」ず「Null可」のクリヌンアップ

V1ではフィヌルドの倀が必須であるこずずNullを入れられるこずが混同されがちでした。V2ではそこを明確にしたす。Null可であるこずは、型定矩で | Noneを远加するこずで衚し、必須であるかどうかはデフォルト倀を蚭定するかどうかで衚したす。

from pydantic import BaseModel

class Foo(BaseModel):
    f1: str  # 必須でNullにするこずはできない
    f2: str | None  # 必須だけどNullを指定するこずは可胜
    f3: str | None = None  # 必須ではなく、Nullを指定するこずも可胜
    f4: str = 'Foobar'  # 必須ではないが、Nullには出来ない

バリデヌタヌ関数の改善

V1ではバリデヌタヌ関数は順番に適甚される圢でしたが、V2では入れ子構造で呌べるようになりたす。これたでは、pre=Trueなどを駆䜿しお順序のコントロヌルをしおいたしたが、V2からは条件によっおは䞍芁なバリデヌタの呌び出しをスキップできたりするのでより効率的にバリデヌションを行えたす。たた、入れ子のバリデヌタがバリデヌション゚ラヌになった時にそれをキャッチしおデフォルト倀を返すずいうこずも出来るようになりたす。

よりパワフルな゚むリアス

こういう䟋が提瀺されおいるのですが、ただ実行環境がなくおよく理解できおいたせん。おそらく「bazのindex=2の芁玠のquxの倀をbarの倀にする」ずいうこずが出来るのだろうず想像しおいたす

from pydantic import BaseModel, Field


class Foo(BaseModel):
    bar: str = Field(aliases=[['baz', 2, 'qux']])


data = {
    'baz': [
        {'qux': 'a'},
        {'qux': 'b'},
        {'qux': 'c'},
        {'qux': 'd'},
    ]
}

foo = Foo(**data)
assert foo.bar == 'c'

ダンプ、シリアラむズ、゚クスポヌトの改善

V2からはmodel.dict()でJSON準拠の型のみを䜿う゚キスポヌトが出来るようになりたす。V1では䟋えば datetime 型はそのたた曞き出されおいお、json.dumps()するず゚ラヌになっちゃいたす盎接model.json()すれば倧䞈倫 。

バリデヌションのコンテキスト

model_validateやmodel_validate_jsonでコンテキスト情報を匕数で枡せるようになりたす。コンテキストは蟞曞型のデヌタで任意のキヌで情報を匕き枡しできたす。

以䞋の䟋では、サポヌトしおいる囜コヌドの情報をDBから取埗しおそれを枡しおいたす。そしおバリデヌタ偎でそれを䜿っお home_countryフィヌルドの囜コヌドがその䞭に含たれおいるかどうかのチェックをしおいたす。

from pydantic import BaseModel, EmailStr, validator

class User(BaseModel):
    email: EmailStr
    home_country: str

    @validator('home_country')
    def check_home_country(cls, v, context):
        if v not in context['countries']:
            raise ValueError('invalid country choice')
        return v

async def add_user(post_data: bytes):
    countries = set(await db_connection.fetch_all('select code from country'))
    user = User.model_validate_json(post_data, context={'countries': countries})
    ...

モデル名前空間のクリヌンアップ

Pydanticで䜜るモデルは pydantic.BaseModelを継承しおいたす。そのため、ここで定矩されおいるメ゜ッド名ず同じフィヌルド名を持぀モデルは䜜れたせんでした。V2ではこれを model_で始たるメ゜ッド名に統䞀しお解りやすくしたす。具䜓的には以䞋のようになりたす

  • .model_validate() 元 .parse_obj()
  • .model_validate_json() 元 .parse_raw(..., content_type='application/json')
  • .model_is_instance()新蚭
  • .model_is_instance_json()新蚭
  • .model_dump() 元 .dict()
  • .model_dump_json() 元 .json()
  • .model_json_schema() 元.schema()
  • .model_update_forward_refs() 元 .update_forward_refs()
  • .model_construct() 元 .construct()
  • .model_customize_schema() 新蚭
  • ModelConfig 元 Config

そしお、以䞋は削陀されたす

  • .parse_file() - Pydanticに入れたの間違いでした
  • .parse_raw() - Jsonを読むのは .model_validate_json()ぞ。それ以倖は間違いでした
  • .from_orm() - configに移動
  • .schema_json() - json.dumps(m.model_json_schema())で眮き換え
  • .copy() - __copy__を実装しおcopyモゞュヌルを぀かっおもらうこずにしたす。

Strict APIずAPIドキュメンテヌション

V2では公開APIず内郚関数やクラスを明確に切り分けたす。内郚のオブゞェクトは _internalサブパッケヌゞに入れお利甚しにくくしおいたす。公開APIにはドキュメントが䜜られたす。mkdocstringsを䜿う予定です。

゚ラヌ蚘述

V2ではより掗緎された゚ラヌ衚瀺がされるようになりたす。その䞭にはそのタむプの゚ラヌの詳现を蚘述したWebペヌゞぞのURLも含たれおいおどんな゚ラヌが起きおいるのかを理解しやすくなっおいたす。

Pure Python

pydantic-coreはRustで曞かれるのでバむナリパッケヌゞがむンストヌルできる特定の環境のみで動䜜したす。サポヌト予定の環境は以䞋の通りです。

  • Linux: x86_64, aarch64, i686, armv7l, musl-x86_64 , musl-aarch64
  • MacOS: x86_64, arm64 (python 3.7を陀く)
  • Windows: amd64, win32
  • Web Assembly: wasm32 wasm䞊のPythonは問題あるみたいだけど、pydantic-coreはwasm32でコンパむルしおナニットテスト通したす

䞀方、PydanticそのものはPure Pythonになりたす。V1では性胜改善のためにcythonを䜿っお利甚環境向けにコンパむルしおいたしたが、V2ではそのようなパヌトは pydantic-coreに寄せられおいたす。これによっおpydanticのコヌドサむズは小さくなり、ナニットテストも速くなりたした。

is_instance的なチェック

.model_is_instance()メ゜ッドによっお、䟋倖凊理を䜿わずにそのデヌタが察象のモデルに倉換可胜かを True/Falseで知るこずが出来るようになりたす。

Parseっお䜿うのをやめるよ

V1では"parse"ずいう蚀葉ず"validate"ずいう蚀葉をメ゜ッド名などで混圚しお䜿っおいたした。Pydanticはバリデヌションだけのラむブラリではないですが、ほずんどの人は "validate"を䜿うのでそちらで統䞀するこずにしたした。

カスタムフィヌルド型の倉曎

バリデヌタヌの呌び出し方を倉曎したので、V1の __get_validators__を䜿ったカスタムフィヌルド型の定矩方法は䜿えなくなっおしたいたした。代わりに、__pydantic_validation_schema__ずいう属性を芋るようにしお、そこに pydantic-core互換のスキヌマを曞けばそれを適甚する圢にしたした。

その他倉曎

  1. 巡回参照による再垰モデルを定矩できたす。V1でも出来たしたが、Pythonの再垰呌び出し䞊限1000に匕っかかりで゚ラヌが出おしたうこずがありたした。V2ではRustで実装されおいお正しく凊理できたす。
  2. pydantic-coreをwasmでコンパむルしお動かすこずにこだわっおいるのは、Pydantic V2のコヌド䟋をブラりザの䞊で線集しお実行出来るようにするためです。
  3. total=Falseの堎合も含めお TypedDictをサポヌトしたした。
  4. from_ormはfrom_attributesずなり、スキヌマ生成時に定矩されるこずになりたすモデルのConfigかフィヌルドのConfigにお
  5. input_valueがValidationErrorに远加されおなぜ゚ラヌが起きたのかがわかりやすくなりたす。
  6. スキヌマでon_errorロゞックを定矩できお、゚ラヌの時にデフォルト倀をセットするのか、その倀を無いこずにするのかを遞べるようになりたした。
  7. datetime, date, time, timedeltaのバリデヌションが改善したした。このために speedateずいうRustのラむブラリを新たに䜜りたした。
  8. 入れ子になったスキヌマの蚭定をマヌゞあるいは䞊曞きするために「優先床」の仕組みができたした。
  9. annotated-typesのサポヌトを行うので、Annotated[set[int], Len(0, 10)]敎数のセット型、ただし芁玠数は0から9たでずか、Annotated[str, Len(1, 1024)]文字列、ただし文字数は1から1023たでずいう型をバリデヌションできたす
  10. 䞀皮類のvalidateずいうデコレヌタを色々な堎所で䜿えたす。
    • 関数 (validate_argumentsの眮き換え)
    • デヌタクラス。pydantic.dataclasses.dataclassはこれの゚むリアスになる
    • TypedDict
    • サポヌトしおいる党おの型。䟋えば、 Union[...], Dict[str, Thing]など
    • カスタムフィヌルド型。__pydantic_schema__ 属性を持぀すべおのもの
  11. バリデヌション゚ラヌを簡単に䜜成する方法を提䟛したす。特にモデルの倖で出来る方法が欲しいどうするかはただ決たっおいない
  12. モデルの __eq__の性胜改善
  13. 蚈算されたフィヌルド。アむディアはずっずあっお、ちゃんずしたものを䜜るべき
  14. サブクラスのむンスタンスがデヌタを挏らさずにモデルをバリデヌションする方法を提䟛したすよくわからない??
  15. バヌゞョンはsemvarのルヌルに埓いたす
  16. ゞェネリックス型で M(GenericModel, Generic[T])ず曞かなければならなかったのを、GenericModelは廃止しお M(BaseModel, Generic[T])ず曞けるようにしたす。

削陀される機胜ず制限

  1. モデルなしでバリデヌションできるようになるので__root__カスタムルヌトモデルは䞍芁になりたす。
  2. .parse_file() は削陀されたす。 .parse_raw()の䞀郚機胜jsonバリデヌションは .model_validate_json()に匕き継がれ、それ以倖は削陀されたす。
  3. .schema_json() ず .copy()
  4. TypeErrorはバリデヌション゚ラヌずしお扱われなくなりたした。関数バリデヌタヌで匕数名の゚ラヌをキャッチするのに内郚で䜿われたす。
  5. 組み蟌み型の str, bytes, intのサブクラスはなくなりたした。これらのバリデヌションをRustで曞いた pydantic-coreでやるためですが、必芁であれば wrap バリデヌタヌかカスタムタむプバリデヌタヌを䜿うこずができたす
  6. 敎数はRustのi64で衚珟されるこずになりたす。これを超える数倀を扱いたければ wrapバリデヌタヌを曞くこずで実珟できたす。
  7. 蚭定管理。機胜をなくす぀もりはないけど、Pydanticずは別のパッケヌゞにするか、Pydanticのextraにしお、pip install pydantic[settings]でむンストヌルできるようにするずかただ決たっおいない暡様
  8. 以䞋のConfigのプロパティは削陀されたす
    • fields - Fieldが出来る前からあったもので、削陀可胜です。
    • allow_mutation - frozen で代甚
    • error_msg_templates - ちゃんずドキュメント化されおおらず、゚ラヌメッセヌゞは倖郚ロゞックでカスタマむズ可胜
    • getter_dict - pydantic-core はハヌドコヌドされた from_attributes ロゞックを持っおいる
    • json_loads - これもpydantic-coreでハヌドコヌドされおいる
    • json_dumps - 倚分消す
    • json_encoders - 䞊のモヌドの議論を参照しおください
    • underscore_attrs_are_private - デフォルト倀で PrivateAttr぀ければ良いだけ
    • smart_union - 党おのunionsは "smart"になりたした

たずめ

Pydantic V2に぀いおたずめおみたした。非互換の倉曎が加わるのでこれたでのコヌドの曞き換えが䞀定必芁になりそうですが、これだけ速床向䞊があるずそれも前向きにできそうです。そしお、速床だけでなく色々ずクリヌンナップされおいるので党䜓的にスッキリ曞けるようになるのではないかず期埅しおいたす。

なお、これたでPythonでパフォヌマンスを䞊げるための手段ずしお SWIG, Pyrex, Cythonなどが提案されおきたしたが、Pydantic V2ではRustを䜿っお速床向䞊するずいう新しい手段を取っおいお、それを可胜にしおいる PyO3ずいうラむブラリにもちょっず興味があるので、時間を芋぀けおそちらも芋おみたいず思いたす。

102
53
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
102
53

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?