(しかしながら厳選なる抽選の結果、チケットはご用意していただけず…。) さて、今回は直近の @niftyメール関連のサービス開発で利用した技術スタックの一部である、pydantic ついてお話したいと思います。
pydanticとは
Python 製ライブラリで、Python の型アノテーションを用いてデータのバリデーションを簡単に行えるようになります。クリーンアーキテクチャのエンティティの実装などで重宝します。 公式ページは以下です。https://pydantic-docs.helpmanual.io/ FastAPI や Django Ninja などで利用されているため、これらのフレームワークを使っている方はご存知かと思います。 私自身も個人的に FastAPI を触っている際に、その使い勝手の良さに心を奪われました。
今回開発したシステムでは Flask を採用しましたが、pydantic は単独で利用しても十分にそのポテンシャルを発揮してくれています。 利用にあたっては、前提として 型ヒント に関する知識は必要になります。 なお、型安全な開発をしたいのであれば静的型付け言語を使えばいいのではないかというご意見もあるかもしれませんが、本記事のスコープではないため割愛いたします。
基本的な使い方
インストール
pip でインストールが可能です。
1 |
pip install pydantic |
モデル作成(基本形)
BaseModel を継承したクラスを記述します。書き方は dataclass と似てますね。
1 2 3 4 5 6 7 8 9 10 |
from datetime import datetime from typing import List, Optional from pydantic import BaseModel class User(BaseModel): id: int name = 'Shinjuku Taro' signup_ts: Optional[datetime] = None friends: List[int] = [] |
変数名 | 型 |
---|---|
id | int |
name | str |
signup_ts | datetime |
friends | List (valueは int) |
1 2 3 4 5 6 7 8 9 10 11 12 |
# 初期化 user = User( id=123, signup_ts='2022-05-19 12:22', friends=[1, 5] ) # フィールドにアクセス print(user.id) # 123 print(user.name) # Shinjuku Taro print(user.signup_ts) # 2022-05-19 12:22:00 print(user.friends) # [1, 5] |
モデル作成(辞書型データの利用)
辞書型の変数を渡して初期化することも可能です。
1 2 3 4 5 6 7 8 9 10 11 12 |
external_data = { 'id': '123', 'signup_ts': '2022-05-19 12:22', 'friends': [1, '5'], } user = User(**external_data) print(user.id) # 123 print(user.name) # Shinjuku Taro print(user.signup_ts) # 2022-05-19 12:22:00 print(user.friends) # [1, 5] |
pydantic では str、bytes、float型を int型の変数に代入する際はできる限り int型にキャストするようになっています。
(変換できない場合は例外が投げられます。) 公式ドキュメントでは このあたり で触れられていますが、Strict Types を使うと入力値も厳密に型チェックができるようになります。
型が異なる場合
各フィールドの型定義と異なる型の値を代入すると実行時に例外が送出されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
from pydantic import ValidationError try: user = User( id='not a number', signup_ts='dummy', friends=[1, 'example'] ) except ValidationError as e: print(e) # 3 validation errors for User # id # value is not a valid integer (type=type_error.integer) # signup_ts # invalid datetime format (type=value_error.datetime) # friends -> 1 # value is not a valid integer (type=type_error.integer) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
try: user = User( id='not a number', signup_ts='dummy', friends=[1, 'example'] ) except ValidationError as e: print(e.json()) # [ # { # "loc": [ # "id" # ], # "msg": "value is not a valid integer", # "type": "type_error.integer" # }, # { # "loc": [ # "signup_ts" # ], # "msg": "invalid datetime format", # "type": "value_error.datetime" # }, # { # "loc": [ # "friends", # 1 # ], # "msg": "value is not a valid integer", # "type": "type_error.integer" # } # ] |
型の種類
Python の組み込み型だけでなく、pydantic オリジナルの便利な型も利用できます。以下は一例です。
- EmailStr:Eメール用の型。
- HttpUrl:http or https で始まる URL に準拠した型。
- SecretStr:センシティブな情報を扱う型。フィールドにアクセスすると '**********'と表示される。
カスタムバリデーション
pydantic の @validator デコレータを使って簡単にフィールド値のバリデーションを行うことができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
from pydantic import BaseModel, ValidationError, validator class UserModel(BaseModel): name: str username: str password1: str password2: str @validator('name') def name_must_contain_space(cls, v): if ' ' not in v: raise ValueError('must contain a space') return v.title() @validator('password2') def passwords_match(cls, v, values, **kwargs): if 'password1' in values and v != values['password1']: raise ValueError('passwords do not match') return v @validator('username') def username_alphanumeric(cls, v): assert v.isalnum(), 'must be alphanumeric' return v |
バリデーションメソッドの書き方の基本
@validator
にはバリデーションを行うフィールド名を渡します。
@validator
が適用されたメソッドの第二引数にはデコレータに渡したフィールドの値が格納されています。引数の名称は任意の値で構いません(ここでは v としています)。
あとはこの v を用いて任意のバリデーションを記述します。 本メソッドは最終的にフィールド値を return するか、バリデーションエラーであれば ValueError 、 TypeError 、 AssertionError 例外を raise します。
では、サンプルコードの3つのバリデーションメソッドについて見ていきましょう。
name
値に半角スペースが含まれていなければ ValueError を返します。シンプルですね。
password2
入力フォームでパスワードを 2回入力してもらい、両者が一致しているかどうかを確認するようなケースを想定しています。バリデーションメソッドの第三引数 (上述の values ) には他のフィールド値が辞書型で保存されているため、 そこから password1 にアクセスしています。 なお、任意のフィールド値のバリデーションで他のフィールド値を利用する際には以下の2点に注意する必要があります。
- バリデーションに失敗したフィールドは values に格納されない。
- バリデーションは基本的にフィールドを定義した順番に行われる。
password1 はカスタムバリデーションを定義していないため、str 型であるかどうかのみしかチェックされませんが、その検証に失敗した場合は values されません。 また、 password1 は password2 の前に定義されているため、2つ目の理由により password1 のバリデーションメソッド内で password2 にアクセスすることはできません。
( password1 のバリデーション時は password2 がバリデーション前だからです。)
1 2 3 4 5 6 |
# この場合、values に password2 は存在しない。 @validator('password1') def passwords_match(cls, v, values, **kwargs): if 'password2' in values and v != values['password2']: raise ValueError('passwords do not match') return v |
username
assert 文を用いて英数字であるかどうかを確認しています。条件に当てはまらない場合は AssertionError が返されます。 このように、フィールド値に対するちょっと複雑なバリデーションも簡潔に実装することができます。
サンプルコードのようにデータとそのバリデーションを同一クラス内に実装することを半ば強制できるので、ロジックが分離するといったような凝集度の低いコードが生まれる可能性を未然に防ぐことができます。
さいごに
pydantic は今回ご紹介した機能以外にも様々な特徴を備えています。- イミュータブルなオブジェクトの生成
- ORM モードの利用で ORM オブジェクトへのマッピング
- BaseSettings の利用によるアプリケーション設定の柔軟な定義
- モデルから JSON スキーマの自動生成
- etc…
本記事が皆さんの安全な Python 開発ライフに少しでもお役に立てましたら幸いです。
We are hiring!
ニフティでは、さまざまなプロダクトへ挑戦するエンジニアを絶賛募集中です!ご興味のある方は以下の採用サイトよりお気軽にご連絡ください! Tech TalkやMeetUpも開催しております!
こちらもお気軽にご応募ください!