記述子は、プロパティへのアクセスを仲介するクラスです。記述子クラスを使用して、プロパティ値を取得、設定、または削除できます。記述子オブジェクトは、クラスの定義時にクラスに組み込まれます。
一般に、記述子はバインディング動作を持つオブジェクト プロパティであり、そのプロパティへのアクセスは記述子プロトコル メソッドによってオーバーライドされます。これらのメソッドは、__get__()、__set__()、および __delete__() です。オブジェクトにこれら 3 つのメソッドが含まれる限り (翻訳者注: 少なくとも 1 つ含まれます)、そのオブジェクトは記述子と呼ばれます。
プロパティ アクセスのデフォルトの動作は、オブジェクトのディクショナリからプロパティを取得 (get)、設定 (set)、削除 (delete) します。例: a.x の検索チェーンは a.__dict__['x'] で始まり、次に type(a).__dict__['x']、次に type(a) のメタクラスを除く基本クラスになります (翻訳者注: 継承ツリーが深く、複数の基本クラスにアクセスできます)。見つかった値が記述子メソッドを含むオブジェクトである場合、Python はデフォルトの動作をオーバーライドして、その記述子メソッドを呼び出すことがあります。記述子は、新しいスタイルのオブジェクトまたは新しいスタイルのクラス (オブジェクトまたは型から継承) でのみ呼び出されることに注意してください。
ディスクリプタは強力な汎用プロトコルです。これらは、プロパティ、メソッド、静的メソッド、クラス メソッド、および super() の背後にある実装メカニズムです。これらは、新しいスタイルのクラスを実装するために Python 2.2 で広く使用されています。記述子は、基礎となる C コードを簡素化し、Python プログラミング用の柔軟な新しいツール セットを提供します。
記述子設計パターンには、所有者クラスとプロパティ記述子自体の 2 つの部分があります。所有者クラスは、そのプロパティに 1 つ以上の記述子を使用します。記述子クラスは、get、set、および delete メソッドの組み合わせを定義します。記述子クラスのインスタンスは、所有者クラスのプロパティになります。
属性は、所有者クラスに基づくメソッド関数です。属性とは異なり、記述子はクラスのインスタンスであり、所有者クラスとは異なります。したがって、記述子は通常、再利用可能な汎用プロパティです。所有者クラスは、同様の動作を持つプロパティを管理するために、異なる記述子クラスの複数のインスタンスを持つことができます。
他のプロパティとは異なり、記述子はクラス レベルで作成されます。これらは __init()__ の初期化中には作成されません。記述子の値は初期化中に設定できますが、記述子は通常、メソッド関数の外でクラスの一部として構築されます。
オーナー クラスが定義されると、各記述子オブジェクトは、異なるクラス レベルの属性にバインドされた記述子クラス インスタンスになります。
記述子として認識されるためには、クラスは次の 3 つのメソッドの任意の組み合わせを実装する必要があります。
場合によっては、記述子クラスには、記述子の内部状態を初期化するために __init__() メソッド関数も必要になります。
次のように、定義されたメソッドに基づいた 2 つの記述子があります。
1. 非データ記述子: この種類の記述子は、__set__() または __delete__()、あるいはその両方を定義します。 __get__() を定義できません。非データ記述子オブジェクトは、式の一部としてよく使用されます。これは呼び出し可能なオブジェクトである場合もあれば、独自のプロパティまたはメソッドを持つ場合もあります。不変の非データ記述子は __set__() を実装する必要がありますが、AttributeError をスローするだけの場合があります。インターフェイスがより柔軟であるため、これらの記述子はシンプルになるように設計されています。
2. データ記述子: この種の記述子は少なくとも __get__() を定義します。通常、__get__() と __set__() を定義して可変オブジェクトを作成します。記述子はほとんど表示されないため、それ以上のプロパティやメソッドを定義することはできません。属性への参照にはデータ記述子があり、そのデータは記述子の __get__()、__set__()、または __delete__() メソッドに委任されます。これらは設計が難しいので、後で説明します。
記述子にはさまざまな使用例があります。内部的には、Python はいくつかの理由で記述子を使用します。
記述子の目的を考えるとき、データが記述子として適切に機能するためには、次の 3 つの一般的な使用例も検討する必要があります。
最初のシナリオを詳しく見ていきます。 __get__() メソッドと __set__() メソッドを使用してデータ記述子を作成する方法を見てみましょう。 __get__() メソッドを使用しない非データ記述子の作成についても見ていきます。
2 番目のケース (オーナー インスタンス内のデータ) は、@property デコレータが何を行うかを示しています。考えられる利点は、記述子には、計算をオーナー クラスから記述子クラスに移動するという従来の機能があることです。これはシャード化されたクラス設計になる傾向があり、最良のアプローチではない可能性があります。計算が非常に複雑な場合は、Strategy モードの方が適している可能性があります。
3 番目のケースは、@staticmethod および @classmethod デコレータがどのように実装されるかを示しています。車輪を再発明する必要はありません。
1. 非データ記述子を使用する
多くの場合、属性値が厳密にバインドされた小さなオブジェクトがあります。この例では、数値が単位にバインドされている動きを見ていきます。
これは、__get__() メソッドを欠いた単純な非データ記述子クラスです。
class UnitValue_1: """Measure and Unit combined.""" def __init__(self, unit): self.value = None self.unit = unit self.default_format = "5.2f" def __set__(self, instance, value): self.value = value def __str__(self): return "{value:{spec}} {unit}" .format(spec=self.default_format, **self.__dict__) def __format__(self, spec="5.2f"): #print( "formatting", spec ) if spec == "": spec = self.default_format return "{value:{spec}} {unit}".format(spec=spec, **self.__dict__)
このクラスは、単純な値のペアを定義します。1 つは変更可能 (値)、もう 1 つは有効な不変オブジェクト (ユニット) です。
この記述子にアクセスすると、記述子オブジェクト自体が使用可能になり、記述子の他のメソッドまたはプロパティを使用できるようになります。この記述子を使用して、物理単位に関連する寸法やその他の値を管理するクラスを作成できます。
以下は速度-時間-距離の初期計算のためのクラスです:
class RTD_1: rate = UnitValue_1("kt") time = UnitValue_1("hr") distance = UnitValue_1("nm") def __init__(self, rate=None, time=None, distance=None): if rate is None: self.time = time self.distance = distance self.rate = distance / time if time is None: self.rate = rate self.distance = distance self.time = distance / rate if distance is None: self.rate = rate self.time = time self.distance = rate * time def __str__(self): return "rate: {0.rate} time: {0.time} distance:{0.distance}".format(self)
オブジェクトが作成され、プロパティが読み込まれると、欠損値が計算されます。計算が完了すると、記述子をチェックして値または単位の名前を取得できます。さらに、記述子には str() の便利な応答および要求形式があります。
次は、記述子と RTD_1 クラス間の対話です:
>>> m1 = RTD_1(rate=5.8, distance=12) >>> str(m1) 'rate: 5.80 kt time: 2.07 hr distance: 12.00 nm' >>> print("Time:", m1.time.value, m1.time.unit) Time: 2.0689655172413794 hr
レートと距離のパラメーターを使用して RTD_1 インスタンスを作成します。これらは、レートおよび距離記述子の計算に使用される __set__() メソッドです。
str(m1) をリクエストすると、RTD_1 のすべての str() メソッドが計算され、代わりにレート、時間、距離記述子の __format__() メソッドが使用されます。これにより、数値と単位が得られます。
非データ記述子には __get__() がなく、内部値を返さないため、記述子の個々の要素にアクセスできます。
2. データ記述子を使用します
データ記述子の設計は、インターフェイスに制限があるため、より複雑になります。 __get__() メソッドが必要で、__set__() または __delete__() のみを含めることができます。これがインターフェイスのすべてです。1 から 3 までのメソッドがあり、他のメソッドはありません。追加のメソッドを導入すると、Python はクラスを適切なデータ記述子として扱わなくなります。
記述子を使用して、__get__() および __set__() メソッドで適切な変換を実行できる単純な単位変換モードを設計します。
以下は、他の単位と標準単位の間で変換を行う単位記述子のスーパークラスです:
class Unit: conversion = 1.0 def __get__(self, instance, owner): return instance.kph * self.conversion def __set__(self, instance, value): instance.kph = value / self.conversion
このクラスは、単純な乗算と除算を使用して、標準単位を他の非標準単位に、またはその逆に変換します。
このスーパークラスを使用すると、標準単位からのいくつかの変換を定義できます。前の例では、標準単位は時速キロメートル (km/h) です。
以下は 2 つの変換記述子です
class Knots(Unit): conversion = 0.5399568 class MPH(Unit): conversion = 0.62137119
継承されたメソッドは非常に便利です。変わるのは変換係数だけです。これらのクラスは、単位変換を伴う値を処理するために使用できます。 MPH またはスワップ可能なノードを処理できます。標準単位、時速キロメートルの単位記述子は次のとおりです:
class KPH(Unit): def __get__(self, instance, owner): return instance._kph def __set__(self, instance, value): instance._kph = value
このクラスは標準を表すため、変換は行われません。プライベート変数インスタンスを使用して、速度の標準値を時速キロメートル単位で保持します。算術変換を回避するのは、簡単な技術的な最適化です。無限再帰を避けるために、パブリック フィールドへの参照を避けることが重要です。
次のクラスは、指定されたサイズに対する一連の変換を提供します:
class Measurement: kph = KPH() knots = Knots() mph = MPH() def __init__(self, kph=None, mph=None, knots=None): if kph: self.kph = kph elif mph: self.mph = mph elif knots: self.knots = knots else: raise TypeError def __str__(self): return "rate: {0.kph} kph = {0.mph} mph = {0.knots} knots".format(self)
各クラスレベルの属性は、さまざまなユニットの記述子です。さまざまな記述子の get および set メソッドは、適切な変換を実行します。このクラスを使用して、さまざまな単位間で速度を変換できます。
以下は、Measurement クラスとの対話の例です:
>>> m2 = Measurement(knots=5.9) >>> str(m2) 'rate: 10.92680006993152 kph = 6.789598762345432 mph = 5.9 knots' >>> m2.kph 10.92680006993152 >>> m2.mph 6.789598762345432
さまざまな記述子を設定して、Measurement クラスのオブジェクトを作成します。最初の例では、ノード記述子を設定します。
表示する値が大きな文字列の場合は、各記述子の __get__() が使用されます。これらのメソッドは、所有者オブジェクトから内部 kph フィールド値を取得し、変換係数を適用して、結果の値を返します。
kph フィールドでも記述子が使用されます。この記述子は変換を行いません。ただし、所有者オブジェクトにキャッシュされたプライベート値を返すだけです。 KPH および Knots 記述子では、所有者クラスが kph 属性を実装する必要があります。