Skip to content

デコレーター

models.py とか config.py とかで、 @ で始まる行(デコレーター)をよく見かける。デコレーターは Python の機能で、関数やメソッドを包んで機能を追加したり、振る舞いを変えたりする。

cf. https://docs.python.org/ja/3/glossary.html#term-decorator

FastAPI の @app.get("/") もデコレーター。ここでは FastAPI というより Python と Pydantic の話として、モデルクラスや設定クラスでよく使うデコレーターをまとめておく。


デコレーターの仕組み

デコレーターの実体は「関数を受け取って、関数を返す」関数。 @ はその適用を短く書くための構文で、次の 2 つは同じ意味になる。

@shout
def greet():
    return "hello"

上は、これと同じ。

def greet():
    return "hello"

greet = shout(greet)

この shout は自作のデコレーター。実体はこんな関数で、元の関数 func を中で呼ぶ wrapper を作って返すことで、「元の処理 + 追加の処理」に差し替えている。

def shout(func):
    def wrapper():
        return func().upper() + "!!"
    return wrapper
>>> greet()
'HELLO!!'

引数付きのデコレーター

@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}"
>>> user = User("Alice", "Smith")
>>> user.full_name
'Alice Smith'

計算結果をキャッシュしたい場合は @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"])
>>> user = User.from_dict({"name": "Alice", "email": "alice@example.com"})
>>> user.name
'Alice'

Note

似たものに @staticmethod もある。こっちは selfcls も受け取らない、クラスに置いてあるだけの普通の関数。


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
>>> Item(price=100, quantity=3).model_dump()
{'price': 100, 'quantity': 3, 'total': 300}

total は自分で値をセットしたわけじゃなく、他のフィールド( pricequantity )から計算されてレスポンスに入ってる。値は保存されず、シリアライズのたびに毎回計算されるので、 price を変えれば total も次の出力から自動で追従する。

素の @property だけだとどうなる?

@computed_field を外して @property だけにすると、 item.total でアクセスはできるけど、 model_dump() には出てこない。

from pydantic import BaseModel


class Item(BaseModel):
    price: int
    quantity: int

    @property
    def total(self) -> int:
        return self.price * self.quantity
>>> item = Item(price=100, quantity=3)
>>> item.total
300
>>> item.model_dump()
{'price': 100, 'quantity': 3}

実例: full-stack-fastapi-template の config.py

FastAPI 公式の full-stack-fastapi-templatebackend/app/core/config.py は、上のデコレーターが実戦でどう使われるかのいいお手本になってる。設定クラス( pydantic-settingsBaseSettings )の中で @computed_field + @property@model_validator(mode="after") が使われてる。

@computed_field + @property は、環境変数で受け取った部品(ユーザー名・パスワード・ホスト名など)から DB 接続 URL を組み立てるのに使われてる。

backend/app/core/config.py(抜粋)
    @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" のままなら警告(本番ならエラー)を出すチェックに使われてる。

backend/app/core/config.py(抜粋)
    @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
backend/app/core/config.py(抜粋)
    @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 の重ねがけをうまく型チェックできないことがあって、そのエラーを抑えるためのコメント。動作には関係ない。