![見出し画像](https://assets.st-note.com/production/uploads/images/167642333/rectangle_large_type_2_471cab8232a589e6730df76e35516dff.png?width=1200)
Python 3: Deep Dive (Part 4 - OOP): ディスクリプタ (セクション8-1/15)
ディスクリプタとは、Pythonのクラスやインスタンスの属性の取得(get)、設定(set)、削除(delete)をカスタマイズできる機能で、プロパティやメソッドなどの「マジック」機能の基盤となるメカニズムです。
データディスクリプタ(__set__を実装)と非データディスクリプタ(__get__のみ実装)の2種類があり、データの保存方法と属性の優先順位に違いがあります。
ディスクリプタはクラス属性として定義され、全インスタンスで共有されるため、各インスタンス固有のデータを管理する場合は、インスタンスをキーとした別のディクショナリなどを使用して、データを個別に保存する必要があります。
Pythonには多くの「マジック」機能があります—プロパティ、メソッド、スロットなど—しかし、その内部を覗くと、それらすべてを支える重要なメカニズムが1つあることがわかります:ディスクリプタです。ディスクリプタを理解することで、Pythonのオブジェクトモデルと、クラス内での属性の管理方法に対する考え方が変わるでしょう。ここでは、ディスクリプタの基礎、データディスクリプタと非データディスクリプタの違い、そして__get__と__set__の最初の考察について見ていきましょう。
ディスクリプタ:基盤となるメカニズム
高レベルでは、ディスクリプタによってクラスやインスタンスの属性を取得、設定、削除する際の動作をカスタマイズすることができます。これらは本質的に、特殊メソッド__get__、set、delete、そしてオプションで__set_name__のうち1つ以上を定義するクラスです。ディスクリプタを使用すると、複数の属性にわたってボイラープレートコードを繰り返し書くことなく、プロパティができることすべて(およびそれ以上)を実現できます。
非データディスクリプタ vs. データディスクリプタ
非データディスクリプタは__get__(および場合によっては__set_name__)を実装しますが、__set__や__delete__は実装しません。
データディスクリプタは__get__と__set__または__delete__を実装します。
この区別は、Pythonの内部属性検索ルールにとって重要です。非データディスクリプタは、属性名がインスタンスディクショナリに存在する場合、そちらを優先します。データディスクリプタは、デフォルトでインスタンスディクショナリの検索よりも常に優先されます。
なぜディスクリプタを使うのか?
よく出てくる例として、2次元の点の座標を整数に強制することがあります。各属性に対してプロパティ(ゲッターとセッター付き)を使用することもできますが、それは面倒になる可能性があります:
class Point2D:
@property
def x(self):
return self._x
@x.setter
def x(self, value):
self._x = int(value)
@property
def y(self):
return self._y
@y.setter
def y(self, value):
self._y = int(value)
これは上手く機能しますが、複数のクラスで多くのこのような整数のみの属性を導入する場合、同様のパターンを至る所でコピー&ペーストすることになります。ディスクリプタを使用すると、この論理を再利用可能なディスクリプタクラス—`IntegerValue`のような—にまとめ、それらを単にクラス属性として割り当てることができます。
しかし、重要な注意点があります:クラス属性はすべてのインスタンス間で共有されます。`x = IntegerValue()`がクラス本体に直接存在する場合、`Point2D`のすべてのインスタンスはその1つの`IntegerValue`ディスクリプタオブジェクトを共有します。ここで__get__と__set__の`instance`パラメータが重要になります:異なるオブジェクト間でデータを上書きしたり共有したりすることなく、各インスタンスのデータを個別に処理することができます。
__get__の最初の考察:非データディスクリプタ
最も単純なディスクリプタは__get__だけを必要とします。例えば:
from datetime import datetime
class TimeUTC:
def __get__(self, instance, owner_class):
return datetime.utcnow().isoformat()
`TimeUTC`をクラス属性として付加すると:
class Logger:
current_time = TimeUTC()
`Logger.current_time`(クラスから)または`l = Logger(); l.current_time`(インスタンスから)を呼び出すと__get__がトリガーされます。メソッドのシグネチャでは:
`self`はディスクリプタ自体(`TimeUTC`インスタンス)です。
`instance`は属性にアクセスしたLoggerインスタンス—またはクラス経由でアクセスした場合は`None`—です。
`owner_class`はLoggerクラスです。
よく見られるロジックとして:
def __get__(self, instance, owner_class):
if instance is None:
return self # クラスからアクセスされた場合はディスクリプタ自体を返す
return datetime.utcnow().isoformat() # インスタンスからアクセスされた場合は値を返す
`instance is None`の時に`self`を返すことで、クラスレベルでディスクリプタオブジェクトを検査または操作しやすくなります。
__set__への移行:データディスクリプタ
値を保存または変更するために、__set__を追加します。これによりディスクリプタはデータディスクリプタに変わります。例えば:
class IntegerValue:
def __set__(self, instance, value):
# データの保存場所は後で決めます
print(f"__set__ called with instance={instance}, value={value}")
def __get__(self, instance, owner_class):
if instance is None:
print("__get__ called from class")
else:
print(f"__get__ called for instance={instance}")
`p.x = 100`のようなことをすると、Pythonはそのディスクリプタの__set__を呼び出し、`p`を`instance`として、`100`を`value`として渡します。しかし、各インスタンスのデータを区別して保持するために、`value`をどこに保存すればよいでしょうか?多くのチュートリアルでは誤って`self._value`に保存していますが、これではすべてのインスタンスが同じ`_value`を共有することになってしまいます—これは望ましくありません!
ディスクリプタには通常、以下のような戦略が必要です:
インスタンス自身のディクショナリに保存する、例:`instance.dict['_x']`(__slots__が使用されている場合や属性名が競合する可能性がある場合は失敗する可能性があります)。
単一ディスクリプタインスタンス vs. 複数クラスインスタンス
微妙ですが重要な点として、ディスクリプタをクラス本体に配置する場合(`class Foo: bar = Descriptor()`)、`Foo`のすべてのインスタンスがその1つの`Descriptor`オブジェクトを共有します。ディスクリプタのロジックは、どの`instance`と相互作用しているかを区別する必要があります。ディスクリプタが各インスタンスに対して本当に別々の状態を必要とする場合は、上記で説明したように、各`instance`をキーとしてデータを慎重に保存する必要があります。
この入門のまとめ
ディスクリプタは、日常のPython使用で現れる強力な機能です—多くの場合、`property`の便利さによって隠されています。独自のディスクリプタクラスを作成することで、コードの重複を減らし、データの検証を確実に行い、属性値を動的に計算し、制御された属性アクセスやキャッシングなどの高度な設計問題を解決することができます。
カスタムディスクリプタの作成が初めての場合は、以下の主要なポイントを覚えておいてください:
非データディスクリプタ(__get__のみ)かデータディスクリプタ(__set__または__delete__を追加)のどちらが必要か決定する。
データの保存場所を決める。インスタンスのディクショナリを使用するか、`instance`をキーとした別のディクショナリを使用するか、`WeakKeyDictionary`を使用するか?
クラス全体で単一のディスクリプタインスタンスを取得することを覚えておくので、オブジェクトごとのデータを処理するには`instance`パラメータに依存する必要があります。
今後のレッションでは、メモリ管理の微妙な点(弱参照vs強参照)や__set_name__のような高度な使用パターンに取り組むことで理解を深めていきます。それまでは、単純なディスクリプタを実験し、__get__と__set__で`instance`がどのように扱われるかを確認してください。そうすれば、すぐにPythonの属性処理における最も優雅なメカニズムの1つを使いこなせるようになるでしょう。
Pythonの内部マジックを解き明かすディスクリプタの探求をお楽しみください!