デコレーター¶
models.py とか config.py とかで、 @ で始まる行(デコレーター)をよく見かける。デコレーターは Python の機能で、関数やメソッドを包んで機能を追加したり、振る舞いを変えたりする。
cf. https://docs.python.org/ja/3/glossary.html#term-decorator
FastAPI の @app.get("/") もデコレーター。ここでは FastAPI というより Python と Pydantic の話として、モデルクラスや設定クラスでよく使うデコレーターをまとめておく。
デコレーターの仕組み¶
デコレーターの実体は「関数を受け取って、関数を返す」関数。 @ はその適用を短く書くための構文で、次の 2 つは同じ意味になる。
上は、これと同じ。
この shout は自作のデコレーター。実体はこんな関数で、元の関数 func を中で呼ぶ wrapper を作って返すことで、「元の処理 + 追加の処理」に差し替えている。
引数付きのデコレーター
@field_validator("new_password") や @model_validator(mode="after") みたいに () 付きで使うデコレーターもある。この場合は「デコレーターを返す関数」を呼んでいて、 1 段ラップが増えてるだけで仕組みは同じ。
Python 標準のデコレーター¶
property¶
@property を付けたメソッドは、 () なしの属性アクセスで呼べるようになる。他の属性から計算した値を、普通の属性みたいに見せたいときに使う。
cf. https://docs.python.org/ja/3/library/functions.html#property
class User:
def __init__(self, first: str, last: str):
self.first = first
self.last = last
@property
def full_name(self) -> str:
return f"{self.first} {self.last}"
計算結果をキャッシュしたい場合は @cached_property が使える: cf. プロパティのキャッシュ(cached_property)
classmethod¶
@classmethod を付けたメソッドは、インスタンスじゃなくてクラス自身を第 1 引数(慣習的に cls )として受け取る。インスタンスを作る前でも呼べるので、「別の形式のデータからインスタンスを作る」ファクトリーメソッドによく使われる。
cf. https://docs.python.org/ja/3/library/functions.html#classmethod
class User:
def __init__(self, name: str, email: str):
self.name = name
self.email = email
@classmethod
def from_dict(cls, data: dict) -> "User":
return cls(name=data["name"], email=data["email"])
Note
似たものに @staticmethod もある。こっちは self も cls も受け取らない、クラスに置いてあるだけの普通の関数。
Pydantic のデコレーター¶
field_validator¶
@field_validator("フィールド名") で、特定のフィールドに独自のバリデーションを追加できる。バリデーターはクラスメソッドとして呼ばれるので、 @classmethod と重ねて付ける( @field_validator が上)。
cf. https://docs.pydantic.dev/latest/concepts/validators/#field-validators
from pydantic import BaseModel, field_validator
class SignUp(BaseModel):
password: str
@field_validator("password")
@classmethod
def check_length(cls, v: str) -> str:
if len(v) < 12:
raise ValueError("password is too short")
return v
zxcvbn でパスワードの強度をチェックする実例は バリデーション(Pydantic)の Field validators セクション に。
model_validator¶
@model_validator(mode="after") は、フィールド単位じゃなくてモデル全体に対するバリデーション。全フィールドの検証が終わった後に呼ばれるので、複数フィールドをまたぐチェックや、他のフィールドの値から初期値を埋めるのに使える。 self を受け取って self を返す。
cf. https://docs.pydantic.dev/latest/concepts/validators/#model-validators
from typing import Self
from pydantic import BaseModel, model_validator
class SignUp(BaseModel):
password: str
password_confirm: str
@model_validator(mode="after")
def check_passwords_match(self) -> Self:
if self.password != self.password_confirm:
raise ValueError("passwords do not match")
return self
Note
mode="before" にすると、フィールドの検証が始まる前(型変換前の生データ)に対して実行される。こっちはクラスメソッドとして書く。
computed_field¶
@computed_field を @property に重ねて付けると( @computed_field が上)、そのプロパティが model_dump() や API レスポンスのシリアライズ結果に含まれるようになる。素の @property だけだとシリアライズには出てこない。
cf. https://docs.pydantic.dev/latest/concepts/fields/#the-computed_field-decorator
from pydantic import BaseModel, computed_field
class Item(BaseModel):
price: int
quantity: int
@computed_field
@property
def total(self) -> int:
return self.price * self.quantity
total は自分で値をセットしたわけじゃなく、他のフィールド( price と quantity )から計算されてレスポンスに入ってる。値は保存されず、シリアライズのたびに毎回計算されるので、 price を変えれば total も次の出力から自動で追従する。
素の @property だけだとどうなる?
@computed_field を外して @property だけにすると、 item.total でアクセスはできるけど、 model_dump() には出てこない。
実例: full-stack-fastapi-template の config.py¶
FastAPI 公式の full-stack-fastapi-template の backend/app/core/config.py は、上のデコレーターが実戦でどう使われるかのいいお手本になってる。設定クラス( pydantic-settings の BaseSettings )の中で @computed_field + @property と @model_validator(mode="after") が使われてる。
@computed_field + @property は、環境変数で受け取った部品(ユーザー名・パスワード・ホスト名など)から DB 接続 URL を組み立てるのに使われてる。
@computed_field # type: ignore[prop-decorator]
@property
def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn:
return PostgresDsn.build(
scheme="postgresql+psycopg",
username=self.POSTGRES_USER,
password=self.POSTGRES_PASSWORD,
host=self.POSTGRES_SERVER,
port=self.POSTGRES_PORT,
path=self.POSTGRES_DB,
)
@model_validator(mode="after") は 2 か所。メール送信者名が未設定ならプロジェクト名で埋めるのと、 SECRET_KEY などがデフォルト値 "changethis" のままなら警告(本番ならエラー)を出すチェックに使われてる。
@model_validator(mode="after")
def _set_default_emails_from(self) -> Self:
if not self.EMAILS_FROM_NAME:
self.EMAILS_FROM_NAME = self.PROJECT_NAME
return self
@model_validator(mode="after")
def _enforce_non_default_secrets(self) -> Self:
self._check_default_secret("SECRET_KEY", self.SECRET_KEY)
self._check_default_secret("POSTGRES_PASSWORD", self.POSTGRES_PASSWORD)
self._check_default_secret(
"FIRST_SUPERUSER_PASSWORD", self.FIRST_SUPERUSER_PASSWORD
)
return self
# type: ignore[prop-decorator] は何?
mypy が @computed_field と @property の重ねがけをうまく型チェックできないことがあって、そのエラーを抑えるためのコメント。動作には関係ない。