目次
16 章

ディスクリプタと __set_name__

property が動作する原理 — __get__/__set__ プロトコルとデータ / 非データディスクリプタ、__set_name__ できれいなバリデーションフィールドを作るところまでまとめます。

第15章 マジックメソッドの深さとプロトコル__getattr__ / __setattr__ が属性アクセスをフックする道具だと見ました。その上にもう一段 — 属性そのものをオブジェクト化する 道具がディスクリプタです。@property@classmethod@staticmethod、ORM の Column、dataclass のフィールドまで — すべてディスクリプタ の上に作られています。

本章は次の第17章 メタクラス — いつ本当に必要か と一対です。両方とも「ライブラリ / フレームワーク作成者の道具」という共通点があり、どんな場合にどちらを選ぶかは第17章で明示されます。

ディスクリプタとは? #

__get____set____delete__ のいずれかが定義されたクラスのインスタンスがディスクリプタです。 クラスの属性として置けば、その属性にアクセスするときディスクリプタのメソッドが自動で呼ばれます。

最も単純なディスクリプタ
class Constant:
    def __init__(self, value):
        self.value = value

    def __get__(self, instance, owner):
        return self.value

class Config:
    PI = Constant(3.14)

c = Config()
print(c.PI)              # 3.14 — Constant.__get__() が呼ばれる
print(Config.PI)         # 3.14 — 同じメソッド、instance が None

c.PI という単純な属性アクセスが 実はメソッド呼び出し に展開されます。この場所にどんなコードでも差し込めます。

__get__ のシグネチャ #

def __get__(self, instance, owner): ...
  • self — ディスクリプタのインスタンス
  • instance — 属性にアクセスしたオブジェクト。クラス自身を介してアクセスすると None
  • owner — その属性を持つクラス

データ vs 非データディスクリプタ #

ここがディスクリプタで一番ややこしい部分です。

種類定義優先順位
データディスクリプタ__set__ または __delete__ を持つインスタンス dict より 優先
非データディスクリプタ__get__ だけを持つインスタンス dict が 優先

実際の違いを見てみると。

データディスクリプタ
class DataDesc:
    def __get__(self, instance, owner):
        return "from descriptor"

    def __set__(self, instance, value):
        pass

class A:
    x = DataDesc()

a = A()
a.__dict__["x"] = "from instance dict"
print(a.x)   # 'from descriptor'  ← ディスクリプタが勝つ
非データディスクリプタ
class NonDataDesc:
    def __get__(self, instance, owner):
        return "from descriptor"

class B:
    x = NonDataDesc()

b = B()
b.__dict__["x"] = "from instance dict"
print(b.x)   # 'from instance dict'  ← インスタンス dict が勝つ

本優先順位がなぜ重要かというと。

  • メソッドは非データディスクリプタ。インスタンスに同名の属性を作るとそちらが優先 (メソッドオーバーライド可能)
  • @property はデータディスクリプタ。ユーザーが同じ名前でインスタンス属性を作っても常に property が動作

@property は実はディスクリプタ #

property の動作
class Circle:
    def __init__(self, radius):
        self.radius = radius

    @property
    def area(self):
        return 3.14 * self.radius ** 2

c = Circle(5)
print(c.area)   # 78.5 — area.__get__() が呼ばれる

@property デコレータは property ディスクリプタクラスのインスタンス を作ります。Circle.area は関数ではなく property オブジェクトです。自分で作ってみると。

property を自作
class my_property:
    def __init__(self, fget):
        self.fget = fget

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return self.fget(instance)

class Circle:
    def __init__(self, radius):
        self.radius = radius

    @my_property
    def area(self):
        return 3.14 * self.radius ** 2

c = Circle(5)
print(c.area)   # 78.5

標準ライブラリの property は、ここに setter / deleter まで備えた形です。

ユーザー定義 — バリデーションディスクリプタ #

よく使うパターンは 属性をバリデーションするディスクリプタ です。

🚫 最初の試み — 衝突
class PositiveInt:
    def __init__(self):
        self.value = 0    # ✗ すべてのインスタンスが同じディスクリプタを共有

    def __get__(self, instance, owner):
        return self.value

    def __set__(self, instance, value):
        if value < 0:
            raise ValueError("負数は不可")
        self.value = value

問題: PositiveInt() という 1 つのディスクリプタインスタンスがすべての使用箇所で 共有 されます。User のインスタンスを 2 つ作ると age が同じ値になります。

解決: 値を インスタンス dict に 保存します。

✅ インスタンスに保存
class PositiveInt:
    def __set_name__(self, owner, name):
        self.name = "_" + name    # バッキングフィールド名

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return getattr(instance, self.name)

    def __set__(self, instance, value):
        if value < 0:
            raise ValueError(f"{self.name[1:]} は負数不可")
        setattr(instance, self.name, value)

class User:
    age = PositiveInt()
    score = PositiveInt()

u = User()
u.age = 30        # OK
u.age = -1        # ✗ ValueError
u.score = 95      # 別のインスタンスに別の値を保存可能

__set_name__ — きれいな名前の自動付与 (3.6+) #

上のコードのカギが __set_name__ です。

def __set_name__(self, owner, name): ...

このメソッドは クラスが定義されるとき に自動で呼ばれます。

  • owner — ディスクリプタを持つクラス (User)
  • name — ディスクリプタが束ねられた属性名 (agescore)

このフックは 3.6 以前 にはなかったので、名前を直接引数として渡す必要がありました。

昔のやり方 — 新しいコードでは使わない
class User:
    age = PositiveInt("age")     # 名前の重複 — DRY 違反
    score = PositiveInt("score")

__set_name__ がその重複を解消してくれます。ユーザーは単に PositiveInt() と書けばよくなります。

より精緻なバリデーション — Generic で #

これを 複数の型のバリデーションディスクリプタ に一般化すると。

バリデーションディスクリプタのベース
class Validator:
    def __set_name__(self, owner, name):
        self.private_name = f"_{name}"

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return getattr(instance, self.private_name)

    def __set__(self, instance, value):
        self.validate(value)
        setattr(instance, self.private_name, value)

    def validate(self, value):
        raise NotImplementedError    # サブクラスが実装

class IntRange(Validator):
    def __init__(self, lo, hi):
        self.lo, self.hi = lo, hi

    def validate(self, value):
        if not isinstance(value, int):
            raise TypeError(f"int が必要、got {type(value).__name__}")
        if not (self.lo <= value <= self.hi):
            raise ValueError(f"{self.lo} ~ {self.hi} の範囲")

class StringLen(Validator):
    def __init__(self, min_len, max_len):
        self.min_len, self.max_len = min_len, max_len

    def validate(self, value):
        if not isinstance(value, str):
            raise TypeError("str が必要")
        if not (self.min_len <= len(value) <= self.max_len):
            raise ValueError("長さの範囲を超えた")

class User:
    age = IntRange(0, 150)
    name = StringLen(1, 50)

u = User()
u.age = 30          # OK
u.age = 999         # ValueError
u.name = ""         # ValueError

同じパターンで正規表現バリデーション、メール形式バリデーションなどをディスクリプタとして作れます。第4部の第24章 Pydantic v2 の深さ が本パターンの産業級の結果物だと思ってください。

ディスクリプタ vs @property — どちらをいつ? #

場面適した道具
1 つのクラスに 1〜2 か所 のバリデーション@property (シンプル)
複数のクラス / 複数のフィールド で同じバリデーションパターンディスクリプタ (再利用)
単純な計算 (read-only)@property または @cached_property
ORM のカラム、フォームのフィールドのようにメタデータまで束ねるときディスクリプタ

日常のコードのほとんどは @property で解決できます。ディスクリプタを直接作る場面は ライブラリ / フレームワーク のコードです。

@cached_property — よく出会う一形態 #

標準ライブラリにすでにあります。

cached_property
from functools import cached_property

class Document:
    def __init__(self, text):
        self.text = text

    @cached_property
    def word_count(self):
        print("計算中...")
        return len(self.text.split())

d = Document("a b c d")
print(d.word_count)   # 計算中... 4
print(d.word_count)   # 4 (キャッシュ — 計算しない)

最初の呼び出しのときだけ計算し、結果をインスタンス dict に保存。以降は通常の属性のように動作します。@property は毎回計算する点が違います。

cached_property非データディスクリプタ です。だから最初の計算後、インスタンス dict に保存された値が優先され、ディスクリプタが再び呼ばれません。

__get__ の呼び出しフロー — 全体像 #

obj.attr 一行が実行されるときに Python が探す順番。

  1. データディスクリプタ (クラスにあって、__set__ または __delete__ を持つ) → 呼び出し
  2. インスタンス dict → そのまま返す
  3. 非データディスクリプタ / クラス属性 → メソッドなら呼び出し、通常なら返す
  4. __getattr__ → フォールバック
  5. AttributeError → 全部失敗

本優先順位が、上で見たデータ vs 非データの違いの正体です。

__delete__ — 削除フック #

del をフック
class Locked:
    def __get__(self, instance, owner):
        return getattr(instance, "_value", None)

    def __set__(self, instance, value):
        instance._value = value

    def __delete__(self, instance):
        raise AttributeError("削除禁止")

class Box:
    x = Locked()

b = Box()
b.x = 5
del b.x   # AttributeError

頻繁に使うわけではありませんが、リソースの後片付け・ロールバックの場面で必要な場合があります。

slot とディスクリプタの関係 #

第8章 dataclass と __slots____slots__ は実は 各スロットをディスクリプタとして 作ります。

slot もディスクリプタ
class Point:
    __slots__ = ("x", "y")

print(Point.x)   # <member 'x' of 'Point' objects>
print(type(Point.x).__set__ is not None)   # True (データディスクリプタ)

だから __slots__ のオブジェクトは __dict__ がなくても属性アクセス / 代入が動作するわけです。

ディスクリプタを使うライブラリ — 一覧表 #

ライブラリ / ツールディスクリプタの居場所
ビルトインpropertyclassmethodstaticmethod
functoolscached_property
dataclassesfield() (比較 / 生成のメタデータの一部)
ORM (SQLAlchemyDjango ORM)ColumnForeignKey、モデルフィールド
フォーム / バリデーション (WTFormsDjango forms)フィールドクラス群
attrsPydanticフィールド定義

ライブラリのコードを読んでいて「なぜこのクラス変数が魔法のように動くのか」と思ったら、ほぼ常にディスクリプタです。第25章 DB 連携 — SQLAlchemy 2.x + Alembic の ORM モデル定義もこの仕組みを活用します。

練習問題 #

  1. PositiveInt() バリデーションディスクリプタを作成してください。__set_name__ で名前を受け取り、_<name> バッキングフィールドに保存します。class User: age = PositiveInt(); score = PositiveInt() で定義したあと、2 つのインスタンスが互いに異なる値を持つか、u.age = -1ValueError になるか確認します。
  2. Validator ベースを作り、その上に EmailField(Validator) を作成してください。validate(value) で正規表現によりメール形式を検証します。class Account: email = EmailField() で使用したとき、不正な値に対して検証が動作します。
  3. @cached_property を自作してください。最初の呼び出しのときに計算後、インスタンス __dict__ に同じ名前で保存すれば、2 回目の呼び出しからはディスクリプタを飛ばしてインスタンス属性が直接返されます。動作が標準の functools.cached_property と同じか確認します。

一行まとめ: ディスクリプタ = __get__ / __set__ / __delete__ のいずれかを持つクラスのインスタンス。データディスクリプタ (__set__ を持つ) > インスタンス dict > 非データディスクリプタの優先順位。@property / メソッド / cached_property / __slots__ がすべてディスクリプタの一形態。__set_name__ (3.6+) で名前を自動取得。日常は @property で十分、ライブラリ / フレームワークのコードだけディスクリプタを直接。

次の章 #

次の 第17章 メタクラス — いつ本当に必要か では、もう一段上 — クラスを作るクラス の役割を扱います。メタクラス vs __init_subclass__ vs ディスクリプタ vs デコレータの分担も合わせて整理します。

X