デスクリプタ ガイド
*******************

著者:
   Raymond Hettinger

問い合わせ先:
   <python at rcn dot com>


目次
^^^^

* デスクリプタ ガイド

  * 入門

    * シンプルな例: 定数を返すデスクリプタ

    * 動的なルックアップ

    * 管理された属性

    * 名前のカスタマイズ

    * まとめ

  * 実践的なサンプル

    * バリデータクラス

    * カスタムバリデータ

    * 実践的なアプリケーション

  * 技術的なチュートリアル

    * 概要

    * 定義と導入

    * デスクリプタプロトコル

    * デスクリプタ呼び出しの概要

    * インスタンスからの呼び出し

    * クラスからの呼び出し

    * superからの呼び出し

    * 呼び出しロジックのまとめ

    * 自動的の名前の伝達

    * ORMのサンプル

  * ピュアPythonの等価実装

    * プロパティ

    * 関数とメソッド

    * メソッドの種類

    * 静的メソッド

    * クラスメソッド

    * メンバーオブジェクトと __slots__

*デスクリプタ* を使うと、オブジェクトの属性検索、保存、削除をカスタマ
イズできます。

本ガイドは4つの大項目から構成されています。

1. 最初の「入門」では基本の紹介をして、シンプルな例から少しずつ概念を
   説明していきます。もしデスクリプタに触れるのが初めての場合はここか
   ら学んでいきましょう。

2. 2つ目のセクションでは完全で実践的なデスクリプタのサンプルを紹介しま
   す。もしデスクリプタの基本を理解しているのであれば、ここから読み始
   めても良いでしょう。

3. 3番目のセクションはより技巧的なチュートリアルを通じて、デスクリプタ
   がどのようなメカニズムで動作しているのかを紹介します。多くの人はこ
   のレベルの理解は不要でしょう。

4. 最後のセクションは、Cで書かれた組み込みのデスクリプタをピュアPython
   で実現する方法を紹介します。このセクションがどのように関数が束縛メ
   ソッドになるのか、 "classmethod()" や "staticmethod()",
   "property()", *__slots__* といった一般的なツールの実装がどのように
   なっているのか興味を持ったら読んでください。


入門
====

この入門では、なるべく基礎的なところから開始し、新しい要素を一つずつ紹
介していきます。


シンプルな例: 定数を返すデスクリプタ
------------------------------------

"Ten" クラスはデスクリプタで、その "__get__()" メソッドは常に定数の
"10" を返します。

   class Ten:
       def __get__(self, obj, objtype=None):
           return 10

このデスクリプタを使うには他のクラスのクラス変数として保存します:

   class A:
       x = 5                       # Regular class attribute
       y = Ten()                   # Descriptor instance

インタラクティブセッションを使って、通常の属性ルックアップと、デスクリ
プタのルックアップの違いを見てみましょう。

   >>> a = A()                     # Make an instance of class A
   >>> a.x                         # Normal attribute lookup
   5
   >>> a.y                         # Descriptor lookup
   10

"a.x" 属性ルックアップではドット演算子がクラス辞書の中から "'x': 5" を
見つけます。 "a.y" ルックアップではドット演算子は "__get__" メソッドを
持つデスクリプタインスタンスを取得します。そのメソッドを呼び出すと、
"10" を返します。

"10" はクラスの辞書にも、インスタンスの辞書にも格納されていません。そ
の代わり、 "10" はオンデマンドに計算されます。

このサンプルは、シンプルなデスクリプタがどのように動作するかを説明する
ためのものですが、実用的ではありません。定数を返す場合、通常の属性アク
セスの方が良いでしょう。

次のセクションでは、より実践的な動的なルックアップを作成します。


動的なルックアップ
------------------

単に定数を返すよりも、なにか処理を実行するデスクリプタの方が面白いでし
ょう。

   import os

   class DirectorySize:

       def __get__(self, obj, objtype=None):
           return len(os.listdir(obj.dirname))

   class Directory:

       size = DirectorySize()              # Descriptor instance

       def __init__(self, dirname):
           self.dirname = dirname          # Regular instance attribute

インタラクティブセッションで見たように、この探索は動的です。実行ごとに
異なる計算を行い、毎回答えが更新されます。

   >>> s = Directory('songs')
   >>> g = Directory('games')
   >>> s.size                              # The songs directory has twenty files
   20
   >>> g.size                              # The games directory has three files
   3
   >>> os.remove('games/chess')            # Delete a game
   >>> g.size                              # File count is automatically updated
   2

このサンプルはデスクリプタの中でどのように処理を行うかだけではなく、
"__get__()" の引数の目的も示しています。 *self* パラメータは *size* で
、 *DirectorySize* のインスタンスです。 *obj* パラメータは *g* や *s*
といった *Directory* のインスタンスです。 *obj* により、 "__get__()"
はターゲットのディレクトリを知れます。 *objtype* パラメータは
*Directory* クラスです。


管理された属性
--------------

デスクリプタで広く使われる用途がインスタンスのデータへのアクセスの管理
です。このデスクリプタはクラス辞書のパブリックな属性に割り当てられます
が、実際のデータはインスタンス辞書のプライベートな属性に保存します。パ
ブリックなアクセスが行われると、デスクリプタの "__get__()" と
"__set__()" メソッドが実行されます。

このサンプルでは *age* はパブリックな属性で、 *_age* はプライベートな
属性です。パブリックな属性がアクセスされると、デスクリプタはルックアッ
プや更新のログ出力をします。

   import logging

   logging.basicConfig(level=logging.INFO)

   class LoggedAgeAccess:

       def __get__(self, obj, objtype=None):
           value = obj._age
           logging.info('Accessing %r giving %r', 'age', value)
           return value

       def __set__(self, obj, value):
           logging.info('Updating %r to %r', 'age', value)
           obj._age = value

   class Person:

       age = LoggedAgeAccess()             # Descriptor instance

       def __init__(self, name, age):
           self.name = name                # Regular instance attribute
           self.age = age                  # Calls __set__()

       def birthday(self):
           self.age += 1                   # Calls both __get__() and __set__()

インタラクティブセッションの結果で見てわかるように、管理された属性の
*age* のログ出力が行われますが、通常の属性の *name* はログ出力されませ
ん。

   >>> mary = Person('Mary M', 30)         # The initial age update is logged
   INFO:root:Updating 'age' to 30
   >>> dave = Person('David D', 40)
   INFO:root:Updating 'age' to 40

   >>> vars(mary)                          # The actual data is in a private attribute
   {'name': 'Mary M', '_age': 30}
   >>> vars(dave)
   {'name': 'David D', '_age': 40}

   >>> mary.age                            # Access the data and log the lookup
   INFO:root:Accessing 'age' giving 30
   30
   >>> mary.birthday()                     # Updates are logged as well
   INFO:root:Accessing 'age' giving 30
   INFO:root:Updating 'age' to 31

   >>> dave.name                           # Regular attribute lookup isn't logged
   'David D'
   >>> dave.age                            # Only the managed attribute is logged
   INFO:root:Accessing 'age' giving 40
   40

このサンプル実装には大きな問題があります。プライベートな名前の *_age*
が *LoggedAgeAccess* クラスの中でハードコードされています。そのため、
どのインスタンスも1つしかこのログ出力される属性が作れず、名前を変更で
きません。次のサンプルでこの問題を解決していきます。


名前のカスタマイズ
------------------

クラスがデスクリプタを使うときに、クラスはそれぞれのデスクリプタにどの
ような変数名を使うかを通知します。

このサンプルでは "Person" クラスは、 *name* と *age* という2つのデスク
リプタインスタンスを持っています。 "Person" が定義されると、
*LoggedAccess* の "__set_name__()" にコールバックを行うため、フィール
ド名を記録できます。それぞれのデスクリプタは、 *public_name* と
*private_name* を持ちます。

   import logging

   logging.basicConfig(level=logging.INFO)

   class LoggedAccess:

       def __set_name__(self, owner, name):
           self.public_name = name
           self.private_name = '_' + name

       def __get__(self, obj, objtype=None):
           value = getattr(obj, self.private_name)
           logging.info('Accessing %r giving %r', self.public_name, value)
           return value

       def __set__(self, obj, value):
           logging.info('Updating %r to %r', self.public_name, value)
           setattr(obj, self.private_name, value)

   class Person:

       name = LoggedAccess()                # First descriptor instance
       age = LoggedAccess()                 # Second descriptor instance

       def __init__(self, name, age):
           self.name = name                 # Calls the first descriptor
           self.age = age                   # Calls the second descriptor

       def birthday(self):
           self.age += 1

インタラクティブセッション上で試すと、 "Person" クラスが
"__set_name__()" を呼び出し、フィールド名が記録されることが分かるでし
ょう。 "vars()" を呼んで、デスクリプタを起動しないでデスクリプタを取得
してみましょう。

   >>> vars(vars(Person)['name'])
   {'public_name': 'name', 'private_name': '_name'}
   >>> vars(vars(Person)['age'])
   {'public_name': 'age', 'private_name': '_age'}

新しいクラスは *name* と *age* の両方でログを出力します。

   >>> pete = Person('Peter P', 10)
   INFO:root:Updating 'name' to 'Peter P'
   INFO:root:Updating 'age' to 10
   >>> kate = Person('Catherine C', 20)
   INFO:root:Updating 'name' to 'Catherine C'
   INFO:root:Updating 'age' to 20

2つの *Person* のインスタンスは、プライベート名しか保持していません。

   >>> vars(pete)
   {'_name': 'Peter P', '_age': 10}
   >>> vars(kate)
   {'_name': 'Catherine C', '_age': 20}


まとめ
------

"__get__()" 、 "__set__()" 、あるいは "__delete__()" メソッドを定義し
たあらゆるオブジェクトが、我々が *descriptor* と呼ぶものです。

それに追加して、デスクリプタは "__set_name__()" メソッドを持つこともで
きます。このメソッドは、デスクリプタが設定されるクラス、あるいはアサイ
ンされたクラス変数を知る必要がある場合に利用されます。もし存在していれ
ば、クラスがデスクリプタでなくても呼ばれます。

デスクリプタは属性のルックアップ中に、ドット演算子によって呼び出されま
す。 "vars(some_class)[descriptor_name]" という書き方によって間接的に
デスクリプタがアクセスされると、デスクリプタを実行することなく、デスク
リプタのインスタンスが返されます。

デスクリプタはクラス変数として利用したときにだけ動作します。インスタン
スに設定しても効果はありません。

デスクリプタは、クラス変数として格納されているオブジェクトが属性ルック
アップ中に発生することを制御できるように、フックを提供することが主なモ
チベーションとなっています。

伝統的には、呼び出し側のクラスが、ルックアップ中に発生することを制御し
てきましたが、デスクリプタはその関係を逆転させ、検索されているデータが
問題に介入できるようにします。

デスクリプタは、Pythonのさまざまなところで利用されています。これにより
、単なる関数は、インスタンスに束縛されたメソッドになります。
"classmethod()", "staticmethod()",  "property()",
"functools.cached_property()" といったツールもすべて、デスクリプタとし
て実装されています。


実践的なサンプル
================

このサンプルでは、発見が極めて難しいデータ破損バグを特定するのに利用可
能な、実践的でパワフルなツールを紹介します。


バリデータクラス
----------------

バリデータは管理された属性アクセスのためのデスクリプタです。データを格
納する前に、新しい値が型が適合しているか、値の範囲制限に適合しているか
を検証します。もし制限に合わなければ、例外を送出し、データが不正な状態
になるのを防ぎます。

"Validator" クラスは *abstract base class* であり、属性管理デスクリプ
タでもあります。

   from abc import ABC, abstractmethod

   class Validator(ABC):

       def __set_name__(self, owner, name):
           self.private_name = '_' + name

       def __get__(self, obj, objtype=None):
           return getattr(obj, self.private_name)

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

       @abstractmethod
       def validate(self, value):
           pass

カスタムバリデータを作成するには "Validator" を継承し、 "validate()"
メソッドを作成して必要に応じてさまざまな制約の検証を行います。


カスタムバリデータ
------------------

ここでは3つの実践的なバリデーションのユーティリティのサンプルを紹介し
ます。

1. "OneOf" は制限された候補の集合に含まれる値かどうかを検証します。

2. "Number" は値が "int" か "float" かどうかを検証します。オプションで
   、値が指定された最小値と最大値に含まれるかも検証します。

3. "String" は値が "str" かどうか検証します。オプションで、指定された
   最小、最大の長さの検証をします。ユーザー定義の 術語 検証も行えます
   。

   class OneOf(Validator):

       def __init__(self, *options):
           self.options = set(options)

       def validate(self, value):
           if value not in self.options:
               raise ValueError(f'Expected {value!r} to be one of {self.options!r}')

   class Number(Validator):

       def __init__(self, minvalue=None, maxvalue=None):
           self.minvalue = minvalue
           self.maxvalue = maxvalue

       def validate(self, value):
           if not isinstance(value, (int, float)):
               raise TypeError(f'Expected {value!r} to be an int or float')
           if self.minvalue is not None and value < self.minvalue:
               raise ValueError(
                   f'Expected {value!r} to be at least {self.minvalue!r}'
               )
           if self.maxvalue is not None and value > self.maxvalue:
               raise ValueError(
                   f'Expected {value!r} to be no more than {self.maxvalue!r}'
               )

   class String(Validator):

       def __init__(self, minsize=None, maxsize=None, predicate=None):
           self.minsize = minsize
           self.maxsize = maxsize
           self.predicate = predicate

       def validate(self, value):
           if not isinstance(value, str):
               raise TypeError(f'Expected {value!r} to be an str')
           if self.minsize is not None and len(value) < self.minsize:
               raise ValueError(
                   f'Expected {value!r} to be no smaller than {self.minsize!r}'
               )
           if self.maxsize is not None and len(value) > self.maxsize:
               raise ValueError(
                   f'Expected {value!r} to be no bigger than {self.maxsize!r}'
               )
           if self.predicate is not None and not self.predicate(value):
               raise ValueError(
                   f'Expected {self.predicate} to be true for {value!r}'
               )


実践的なアプリケーション
------------------------

次のコードは実際のクラスでデータバリデータを使う方法のサンプルです。

   class Component:

       name = String(minsize=3, maxsize=10, predicate=str.isupper)
       kind = OneOf('wood', 'metal', 'plastic')
       quantity = Number(minvalue=0)

       def __init__(self, name, kind, quantity):
           self.name = name
           self.kind = kind
           self.quantity = quantity

これらのデスクリプタは、不正インスタンスが作成されるのを防ぎます。

   >>> Component('Widget', 'metal', 5)      # Blocked: 'Widget' is not all uppercase
   Traceback (most recent call last):
       ...
   ValueError: Expected <method 'isupper' of 'str' objects> to be true for 'Widget'

   >>> Component('WIDGET', 'metle', 5)      # Blocked: 'metle' is misspelled
   Traceback (most recent call last):
       ...
   ValueError: Expected 'metle' to be one of {'metal', 'plastic', 'wood'}

   >>> Component('WIDGET', 'metal', -5)     # Blocked: -5 is negative
   Traceback (most recent call last):
       ...
   ValueError: Expected -5 to be at least 0
   >>> Component('WIDGET', 'metal', 'V')    # Blocked: 'V' isn't a number
   Traceback (most recent call last):
       ...
   TypeError: Expected 'V' to be an int or float

   >>> c = Component('WIDGET', 'metal', 5)  # Allowed:  The inputs are valid


技術的なチュートリアル
======================

これから先はデスクリプタのメカニズムや動作の詳細について、よりテクニカ
ルなチュートリアルになります。


概要
----

デスクリプタの定義、プロトコルのサマリー、デスクリプタがどのように呼び
出されるかの説明、ORマッパーがどのように動作するかのサンプルを提示しま
す。

デスクリプタについて学ぶことにより、新しいツールセットが使えるようにな
るだけでなく、Python の仕組みについてのより深い理解が得られます。


定義と導入
----------

一般に、デスクリプタは、デスクリプタプロトコルで定義されたメソッドを持
つ属性値です。このメソッドは、 "__get__()", "__set__()", および
"__delete__()" です。これらのメソッドのいずれかが、オブジェクトに定義
されていれば、それは *デスクリプタ* と呼ばれます。

属性アクセスのデフォルトの振る舞いは、オブジェクトの辞書の属性の取得、
設定、削除です。例えば "a.x" は、まず "a.__dict__['x']"、それから
"type(a).__dict__['x']"、さらに "type(a)" のメソッド探索順序に従ったさ
らなる探索へと連鎖します。見つかった値が、デスクリプタメソッドのいずれ
かを定義しているオブジェクトなら、Python はそのデフォルトの振る舞いを
オーバーライドし、代わりにデスクリプタメソッドを呼び出します。これがど
の連鎖順位で行われるかは、どのデスクリプタメソッドが定義されているかに
依ります。

デスクリプタは、強力な、多目的のプロトコルです。これはプロパティ、メソ
ッド、静的メソッド、クラスメソッド、そして "super()" の背後にある機構
です。Python のいたるところで使われています。デスクリプタは、基幹にあ
る C コードを簡潔にし、毎日の Python プログラムに、柔軟な新しいツール
群を提供します。


デスクリプタプロトコル
----------------------

"descr.__get__(self, obj, type=None)"

"descr.__set__(self, obj, value)"

"descr.__delete__(self, obj)"

これで全てです。これらのメソッドのいずれかを定義すれば、オブジェクトは
デスクリプタとみなされ、探索された際のデフォルトの振る舞いをオーバーラ
イドできます。

あるオブジェクトが "__set__()" と "__delete__()" の両方を定義していた
ら、それはデータデスクリプタとみなされます。 "__get__()" だけを定義し
ているデスクリプタは、非データデスクリプタと呼ばれます (これらはよくメ
ソッドに使われますが、他の使い方も出来ます)。

データデスクリプタと非データデスクリプタでは、オーバーライドがインスタ
ンスの辞書のエントリに関してどのように計算されるかが異なります。インス
タンスの辞書に、データデスクリプタと同名の項目があれば、データデスクリ
プタの方が優先されます。インスタンスの辞書に、非データデスクリプタと同
名の項目があれば、辞書の項目の方が優先されます。

読み出し専用のデータデスクリプタを作るには、 "__get__()" と
"__set__()" の両方を定義し、 "__set__()" が呼び出されたときに
"AttributeError" が送出されるようにしてください。例外を送出する
"__set__()" メソッドをプレースホルダとして定義すれば、データデスクリプ
タにするのに十分です。


デスクリプタ呼び出しの概要
--------------------------

"desc.__get__(obj)" や "desc.__get__(None, cls)" を使うとデスクリプタ
を直接呼び出すことができます。

しかし、属性アクセスの際に自動的に呼び出されるのが、一般的なデスクリプ
タの呼び出し方法です。

式 "obj.x" は "obj" の名前空間のチェーンの中で属性 "x" を探索します。
もし、その検索のロジックの中で、インスタンスの "__dict__" の外でデスク
リプタを見つけた場合、探索ルールのリストのリストに従って、そのデスクリ
プタの "__get__()" メソッドが呼び出されます。

呼び出しの詳細は、"obj" がオブジェクトかクラスか、superのインスタンス
かに依ります。


インスタンスからの呼び出し
--------------------------

名前空間のチェーンの中で行われるインスタンスルックアップでは、データデ
スクリプタが最優先で、次にインスタンス変数、その次にノンデータデスクリ
プタ、クラス変数、最後に存在したら "__getattr__()" が呼ばれます。

"a.x" に対するデスクリプタが見つかった場合、 "desc.__get__(a,
type(a))" という形式で呼び出されます。

ドットによるルックアップのロジックは"object.__getattribute__()" の中で
行われます。ピュアPythonで表した等価なロジックは次の通りです。

   def find_name_in_mro(cls, name, default):
       "Emulate _PyType_Lookup() in Objects/typeobject.c"
       for base in cls.__mro__:
           if name in vars(base):
               return vars(base)[name]
       return default

   def object_getattribute(obj, name):
       "Emulate PyObject_GenericGetAttr() in Objects/object.c"
       null = object()
       objtype = type(obj)
       cls_var = find_name_in_mro(objtype, name, null)
       descr_get = getattr(type(cls_var), '__get__', null)
       if descr_get is not null:
           if (hasattr(type(cls_var), '__set__')
               or hasattr(type(cls_var), '__delete__')):
               return descr_get(cls_var, obj, objtype)     # data descriptor
       if hasattr(obj, '__dict__') and name in vars(obj):
           return vars(obj)[name]                          # instance variable
       if descr_get is not null:
           return descr_get(cls_var, obj, objtype)         # non-data descriptor
       if cls_var is not null:
           return cls_var                                  # class variable
       raise AttributeError(name)

"__getattribute__()" のコードには "__getattr__()" のフックが無いことに
注意してください。このため、直接 "__getattribute__()" を呼び出すか、
"super().__getattribute__" で呼び出すと、 "__getattr__()" が完全にバイ
パスされます。

その代わりに、 "__getattribute__()" が "AttributeError" を発出したとき
に "__getattr__()" を呼び出すのは、ドット演算子と "getattr()" の責任で
す。これらのロジックはヘルパー関数にカプセル化されています:

   def getattr_hook(obj, name):
       "Emulate slot_tp_getattr_hook() in Objects/typeobject.c"
       try:
           return obj.__getattribute__(name)
       except AttributeError:
           if not hasattr(type(obj), '__getattr__'):
               raise
       return type(obj).__getattr__(obj, name)             # __getattr__


クラスからの呼び出し
--------------------

"A.x" のようなドットを使ったルックアップのロジックは
"type.__getattribute__()" の中にあります。ロジックのステップは
"object.__getattribute__()" と似ていますが、インスタンス辞書のルックア
ップはクラスの *method resolution order* を通じた探索と置き換えられて
います。

"a.x" に対するデスクリプタが見つかった場合、 "desc.__get__(None, A)"
という形式で呼び出されます。

完全な C での実装は Objects/typeobject.c の中の
:c:func:*!type_getattro* と "_PyType_Lookup()" を参照してください。


superからの呼び出し
-------------------

The logic for super's dotted lookup is in the "__getattribute__()"
method for object returned by "super()".

"super(A, obj).m" のようなドットを使ったルックアップは
"obj.__class__.__mro__" を探索して、 "A" の直前のクラス "B" をまず探し
、 "B.__dict__['m'].__get__(obj, A)" を返します。もしデスクリプタでな
ければ "m" を変更せずに返します。

完全な C での実装は Objects/typeobject.c の中の "super_getattro()" を
参照してください。純粋な Python での同等なコードは >>`<<Guido's
Tutorial <https://www.python.org/download/releases/2.2.3/descrintro/#
cooperation>`_を参照してください。


呼び出しロジックのまとめ
------------------------

デスクリプタのメカニズムは、 "object" 、 "type" 、 "super()" の
"__getattribute__()" メソッドの中に組み込まれています。

憶えておくべき重要な点は:

* デスクリプタは "__getattribute__()" メソッドによって呼び出される

* クラスはこの機構を "object" 、 "type" 、 "super()" から継承する

* すべてのデスクリプタのロジックが "__getattribute__()" にあるため、こ
  のメソッドをオーバーライドすると、すべての自動的なデスクリプタ呼び出
  しは抑制される

* "object.__getattribute__()" と "type.__getattribute__()" は
  "__get__()" の呼び出し方が異なる。前者はインスタンスとおそらくメソッ
  ドを含む。後者はインスタンスとして "None" を渡し、クラスは常に渡され
  る

* データデスクリプタは、必ずインスタンス辞書をオーバーライドする

* 非データデスクリプタは、インスタンス辞書にオーバーライドされることが
  ある


自動的の名前の伝達
------------------

デスクリプタにとって、自分がアサインされるクラス変数名が知りたいことが
あります。新しいクラスが作られると、 "type" メタクラスは新しいクラスの
辞書をスキャンします。もし、要素がデスクリプタで、なおかつ
"__set_name__()" メソッドを持っている場合、このメソッドは2つの引数を伴
って呼び出されます。 *owner* はデスクリプタが使用されたクラスで、
*name* はデスクリプタがアサインされたクラス変数の名前です。

実装の詳細は Objects/typeobject.c の中の "type_new()" と "set_names()"
を参照してください。

更新のロジックは "type.__new__()" にあり、通知はクラス作成のタイミング
にしかありません。もしデスクリプタが後からクラスに追加された場合は、
"__set_name__()" を手動で呼び出す必要があるでしょう。


ORMのサンプル
-------------

以下のコードは、 オブジェクト関係マッピング を実装するのにデータディス
クリプタを使う方法を示した、簡略化された骨格です。

基本的な考え方は、データが外部のデータベースに保存することです。
Pythonインスタンスは、データベースのテーブルへのキーのみを保持します。
デスクリプタがルックアップや更新をします。

   class Field:

       def __set_name__(self, owner, name):
           self.fetch = f'SELECT {name} FROM {owner.table} WHERE {owner.key}=?;'
           self.store = f'UPDATE {owner.table} SET {name}=? WHERE {owner.key}=?;'

       def __get__(self, obj, objtype=None):
           return conn.execute(self.fetch, [obj.key]).fetchone()[0]

       def __set__(self, obj, value):
           conn.execute(self.store, [value, obj.key])
           conn.commit()

"Field" クラスを使って、データベースのテーブルごとのスキーマを説明する
ための モデル を定義します。

   class Movie:
       table = 'Movies'                    # Table name
       key = 'title'                       # Primary key
       director = Field()
       year = Field()

       def __init__(self, key):
           self.key = key

   class Song:
       table = 'Music'
       key = 'title'
       artist = Field()
       year = Field()
       genre = Field()

       def __init__(self, key):
           self.key = key

モデルを使用するにはまずデータベースに接続します:

   >>> import sqlite3
   >>> conn = sqlite3.connect('entertainment.db')

次のインタラクティブセッションでは、データベースからのデータの取得と、
更新方法を示しています。

   >>> Movie('Star Wars').director
   'George Lucas'
   >>> jaws = Movie('Jaws')
   >>> f'Released in {jaws.year} by {jaws.director}'
   'Released in 1975 by Steven Spielberg'

   >>> Song('Country Roads').artist
   'John Denver'

   >>> Movie('Star Wars').director = 'J.J. Abrams'
   >>> Movie('Star Wars').director
   'J.J. Abrams'


ピュアPythonの等価実装
======================

このプロトコルは単純ですが、ワクワクする可能性も秘めています。ユースケ
ースの中には、あまりに一般的なので組み込みに追加されたものもあります。
プロパティ、束縛のメソッド、静的メソッド、クラスメソッド、 __slots__
は、全てデスクリプタプロトコルに基づいています。


プロパティ
----------

"property()" を呼び出すことで、属性へアクセスすると関数の呼び出しを引
き起こす、データデスクリプタを簡潔に組み立てられます。シグネチャはこう
です:

   property(fget=None, fset=None, fdel=None, doc=None) -> property

このドキュメントでは、管理された属性 "x" を定義する典型的な使用法を示
します:

   class C:
       def getx(self): return self.__x
       def setx(self, value): self.__x = value
       def delx(self): del self.__x
       x = property(getx, setx, delx, "I'm the 'x' property.")

デスクリプタの見地から "property()" がどのように実装されているかを見る
ために、等価な Python 版をここに挙げます:

   class Property:
       "Emulate PyProperty_Type() in Objects/descrobject.c"

       def __init__(self, fget=None, fset=None, fdel=None, doc=None):
           self.fget = fget
           self.fset = fset
           self.fdel = fdel
           if doc is None and fget is not None:
               doc = fget.__doc__
           self.__doc__ = doc
           self._name = ''

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

       def __get__(self, obj, objtype=None):
           if obj is None:
               return self
           if self.fget is None:
               raise AttributeError(
                   f'property {self._name!r} of {type(obj).__name__!r} object has no getter'
                )
           return self.fget(obj)

       def __set__(self, obj, value):
           if self.fset is None:
               raise AttributeError(
                   f'property {self._name!r} of {type(obj).__name__!r} object has no setter'
                )
           self.fset(obj, value)

       def __delete__(self, obj):
           if self.fdel is None:
               raise AttributeError(
                   f'property {self._name!r} of {type(obj).__name__!r} object has no deleter'
                )
           self.fdel(obj)

       def getter(self, fget):
           prop = type(self)(fget, self.fset, self.fdel, self.__doc__)
           prop._name = self._name
           return prop

       def setter(self, fset):
           prop = type(self)(self.fget, fset, self.fdel, self.__doc__)
           prop._name = self._name
           return prop

       def deleter(self, fdel):
           prop = type(self)(self.fget, self.fset, fdel, self.__doc__)
           prop._name = self._name
           return prop

組み込みの "property()" 関数は、ユーザインターフェースへの属性アクセス
が与えられ、続く変更がメソッドの介入を要求するときに役立ちます。

例えば、スプレッドシートクラスが、"Cell('b10').value" でセルの値を取得
できるとします。続く改良により、プログラムがアクセスの度にセルの再計算
をすることを要求しました。しかしプログラマは、その属性に直接アクセスす
る既存のクライアントコードに影響を与えたくありません。この解決策は、
property データデスクリプタ内に値属性へのアクセスをラップすることです:

   class Cell:
       ...

       @property
       def value(self):
           "Recalculate the cell before returning value"
           self.recalc()
           return self._value

組み込みの "property()" も、この "Property()" も、このサンプルの中で同
じように利用できます。


関数とメソッド
--------------

Python のオブジェクト指向機能は、関数に基づく環境の上に構築されていま
す。非データデスクリプタを使って、この 2 つはシームレスに組み合わされ
ています。

クラス辞書に格納された関数は、実行時にメソッドに変換されます。メソッド
と通常の関数はオブジェクトのインスタンスが他の引数よりも前に渡される点
だけが異なります。監修ではインスタンスは *self* という名前が使われます
が、 *this* や他の変数名も使えます。

メソッドは、ほぼ次のコードと同等な "types.MethodType" を使えば手動で作
れます。

   class MethodType:
       "Emulate PyMethod_Type in Objects/classobject.c"

       def __init__(self, func, obj):
           self.__func__ = func
           self.__self__ = obj

       def __call__(self, *args, **kwargs):
           func = self.__func__
           obj = self.__self__
           return func(obj, *args, **kwargs)

メソッドの自動作成をサポートするには、関数に対し、属性アクセスでメソッ
ドの束縛に利用される "__get__()" メソッドを追加します。これはつまり、
この関数はドットアクセスされたときに束縛されたメソッドを返す非データデ
スクリプタであることを意味します。次のように動作します。

   class Function:
       ...

       def __get__(self, obj, objtype=None):
           "Simulate func_descr_get() in Objects/funcobject.c"
           if obj is None:
               return self
           return MethodType(self, obj)

次のクラスをインタプリタを起動すると、この関数デスクリプタが実際にどう
働くかを見られます:

   class D:
       def f(self, x):
            return x

この関数はイントロスペクションをサポートするために *qualified name* も
持っています。

   >>> D.f.__qualname__
   'D.f'

クラス辞書を通じて関数にアクセスすると "__get__()" が実行されません。
そのため、裏にある関数オブジェクトをそのまま返します。

   >>> D.__dict__['f']
   <function D.f at 0x00C45070>

クラスのドットアクセスは "__get__()" は呼び出しますが、これも裏にある
関数を変更せずに返します。

   >>> D.f
   <function D.f at 0x00C45070>

インスタンスに対してドットアクセスをすると面白い動作をします。ドットの
ルックアップは、束縛メソッドオブジェクトを返す "__get__()" を呼び出し
ます。

   >>> d = D()
   >>> d.f
   <bound method D.f of <__main__.D object at 0x00B18C90>>

内部では、束縛メソッドはオリジナルの関数と束縛されたインスタンスを保持
しています。

   >>> d.f.__func__
   <function D.f at 0x00C45070>

   >>> d.f.__self__
   <__main__.D object at 0x00B18C90>

もし、通常のメソッドの *self* や、クラスメソッドの *cls* がどこから来
るの疑問に思っている人がいるとしたら、これがその答えになります！


メソッドの種類
--------------

非データデスクリプタは、関数をメソッドに束縛する、各種の一般的なパター
ンに、単純な機構を提供します。

まとめると、関数は "__get__()" メソッドを持ち、属性としてアクセスされ
たとき、メソッドに変換されます。この非データディスクリプタは、
"obj.f(*args)" の呼び出しを "f(obj, *args)" に変換します。
"cls.f(*args)" を呼び出すと "f(*args)" になります。

このチャートは、束縛と、その 2 つの異なる便利な形をまとめています:

   +-------------------+------------------------+--------------------+
   | 変換              | オブジェクトから呼び出 | クラスから呼び出さ |
   |                   | される                 | れる               |
   |===================|========================|====================|
   | 関数              | f(obj, *args)          | f(*args)           |
   +-------------------+------------------------+--------------------+
   | 静的メソッド      | f(*args)               | f(*args)           |
   +-------------------+------------------------+--------------------+
   | クラスメソッド    | f(type(obj), *args)    | f(cls, *args)      |
   +-------------------+------------------------+--------------------+


静的メソッド
------------

静的メソッドは、下にある関数をそのまま返します。 "c.f" や "C.f" は、
"object.__getattribute__(c, "f")" や "object.__getattribute__(C, "f")"
を直接探索するのと同じです。結果として、関数はオブジェクトとクラスから
同じようにアクセスできます。

静的メソッドにすると良いのは、 "self" 変数への参照を持たないメソッドで
す。

例えば、統計パッケージに、実験データのコンテナがあるとします。そのクラ
スは、平均、メジアン、その他の、データに依る記述統計を計算する標準メソ
ッドを提供します。しかし、概念上は関係があっても、データには依らないよ
うな便利な関数もあります。例えば、 "erf(x)" は統計上の便利な変換ルーチ
ンですが、特定のデータセットに直接には依存しません。これは、オブジェク
トからでもクラスからでも呼び出せます: "s.erf(1.5) --> .9332" または
"Sample.erf(1.5) --> .9332" 。

静的メソッドは下にある関数をそのまま返すので、呼び出しの例は面白くあり
ません:

   class E:
       @staticmethod
       def f(x):
           return x * 10

   >>> E.f(3)
   30
   >>> E().f(3)
   30

非データデスクリプタプロトコルを使うと、pure Python 版の
"staticmethod()" は以下のようになります:

   import functools

   class StaticMethod:
       "Emulate PyStaticMethod_Type() in Objects/funcobject.c"

       def __init__(self, f):
           self.f = f
           functools.update_wrapper(self, f)

       def __get__(self, obj, objtype=None):
           return self.f

       def __call__(self, *args, **kwds):
           return self.f(*args, **kwds)

"functools.update_wrapper()" の呼び出しは、根底にある関数を参照する
"__wrapped__" 属性を追加します。また、ラッパーがそのラップされた関数の
ように見えるよう、必要な属性を追加します: "__name__" 、 "__qualname__"
、 "__doc__" 、 "__annotations__" 。


クラスメソッド
--------------

静的メソッドとは違って、クラスメソッドは関数を呼び出す前にクラス参照を
引数リストの先頭に加えます。このフォーマットは、呼び出し元がオブジェク
トでもクラスでも同じです:

   class F:
       @classmethod
       def f(cls, x):
           return cls.__name__, x

   >>> F.f(3)
   ('F', 3)
   >>> F().f(3)
   ('F', 3)

この振る舞いは、関数がクラス参照のみを必要とし、特定のインスタンスに保
存されたデータに依存しないときに便利です。クラスメソッドの使い方の一つ
は、代わりのクラスコンストラクタを作ることです。例えば、クラスメソッド
"dict.fromkeys()" は新しい辞書をキーのリストから生成します。等価な
pure Python 版は:

   class Dict(dict):
       @classmethod
       def fromkeys(cls, iterable, value=None):
           "Emulate dict_fromkeys() in Objects/dictobject.c"
           d = cls()
           for key in iterable:
               d[key] = value
           return d

これで一意なキーを持つ新しい辞書が以下のように構成できます:

   >>> d = Dict.fromkeys('abracadabra')
   >>> type(d) is Dict
   True
   >>> d
   {'a': None, 'b': None, 'r': None, 'c': None, 'd': None}

非データデスクリプタプロトコルを使った、 "classmethod()" の pure
Python 版はこのようになります:

   import functools

   class ClassMethod:
       "Emulate PyClassMethod_Type() in Objects/funcobject.c"

       def __init__(self, f):
           self.f = f
           functools.update_wrapper(self, f)

       def __get__(self, obj, cls=None):
           if cls is None:
               cls = type(obj)
           if hasattr(type(self.f), '__get__'):
               # This code path was added in Python 3.9
               # and was deprecated in Python 3.11.
               return self.f.__get__(cls, cls)
           return MethodType(self.f, cls)

The code path for "hasattr(type(self.f), '__get__')" was added in
Python 3.9 and makes it possible for "classmethod()" to support
chained decorators.  For example, a classmethod and property could be
chained together.  In Python 3.11, this functionality was deprecated.

   class G:
       @classmethod
       @property
       def __doc__(cls):
           return f'A doc for {cls.__name__!r}'

   >>> G.__doc__
   "A doc for 'G'"

"ClassMethod" 内の "functools.update_wrapper()" の呼び出しは、根底にあ
る関数を参照する "__wrapped__" 属性を追加します。また、ラッパーがその
ラップされた関数のように見えるよう、必要な属性を追加します: "__name__"
、 "__qualname__" 、 "__doc__" 、 "__annotations__" 。


メンバーオブジェクトと __slots__
--------------------------------

クラスが "__slots__" を定義すると、インスタンスの辞書が固定超のスロッ
トの値の配列と置き換えられます。ユーザーの目線からみると、いくつかの変
化があります。

1. Provides immediate detection of bugs due to misspelled attribute
assignments.  Only attribute names specified in "__slots__" are
allowed:

   class Vehicle:
       __slots__ = ('id_number', 'make', 'model')

   >>> auto = Vehicle()
   >>> auto.id_nubmer = 'VYE483814LQEX'
   Traceback (most recent call last):
       ...
   AttributeError: 'Vehicle' object has no attribute 'id_nubmer'

2. Helps create immutable objects where descriptors manage access to
private attributes stored in "__slots__":

   class Immutable:

       __slots__ = ('_dept', '_name')          # Replace the instance dictionary

       def __init__(self, dept, name):
           self._dept = dept                   # Store to private attribute
           self._name = name                   # Store to private attribute

       @property                               # Read-only descriptor
       def dept(self):
           return self._dept

       @property
       def name(self):                         # Read-only descriptor
           return self._name

   >>> mark = Immutable('Botany', 'Mark Watney')
   >>> mark.dept
   'Botany'
   >>> mark.dept = 'Space Pirate'
   Traceback (most recent call last):
       ...
   AttributeError: property 'dept' of 'Immutable' object has no setter
   >>> mark.location = 'Mars'
   Traceback (most recent call last):
       ...
   AttributeError: 'Immutable' object has no attribute 'location'

3. Saves memory.  On a 64-bit Linux build, an instance with two
attributes takes 48 bytes with "__slots__" and 152 bytes without.
This flyweight design pattern likely only matters when a large number
of instances are going to be created.

4. Improves speed.  Reading instance variables is 35% faster with
"__slots__" (as measured with Python 3.10 on an Apple M1 processor).

5. Blocks tools like "functools.cached_property()" which require an
instance dictionary to function correctly:

   from functools import cached_property

   class CP:
       __slots__ = ()                          # Eliminates the instance dict

       @cached_property                        # Requires an instance dict
       def pi(self):
           return 4 * sum((-1.0)**n / (2.0*n + 1.0)
                          for n in reversed(range(100_000)))

   >>> CP().pi
   Traceback (most recent call last):
     ...
   TypeError: No '__dict__' attribute on 'CP' instance to cache 'pi' property.

"__slots__" はC構造体に直接アクセスし、オブジェクトのメモリ確保を制御
する必要があるため、ピュアPythonバージョンを作成することはできません。
しかし、プライベートの "__slotvalues" リストを使ってC構造体のスロット
をほぼ忠実にエミュレートすることはできます。そのプライベートな構造体へ
の読み書きはメンバーのデスクリプタによって管理されます。

   null = object()

   class Member:

       def __init__(self, name, clsname, offset):
           'Emulate PyMemberDef in Include/structmember.h'
           # Also see descr_new() in Objects/descrobject.c
           self.name = name
           self.clsname = clsname
           self.offset = offset

       def __get__(self, obj, objtype=None):
           'Emulate member_get() in Objects/descrobject.c'
           # Also see PyMember_GetOne() in Python/structmember.c
           if obj is None:
               return self
           value = obj._slotvalues[self.offset]
           if value is null:
               raise AttributeError(self.name)
           return value

       def __set__(self, obj, value):
           'Emulate member_set() in Objects/descrobject.c'
           obj._slotvalues[self.offset] = value

       def __delete__(self, obj):
           'Emulate member_delete() in Objects/descrobject.c'
           value = obj._slotvalues[self.offset]
           if value is null:
               raise AttributeError(self.name)
           obj._slotvalues[self.offset] = null

       def __repr__(self):
           'Emulate member_repr() in Objects/descrobject.c'
           return f'<Member {self.name!r} of {self.clsname!r}>'

"type.__new__()" メソッドはクラス変数へのメンバーオブジェクトの追加を
処理します。

   class Type(type):
       'Simulate how the type metaclass adds member objects for slots'

       def __new__(mcls, clsname, bases, mapping, **kwargs):
           'Emulate type_new() in Objects/typeobject.c'
           # type_new() calls PyTypeReady() which calls add_methods()
           slot_names = mapping.get('slot_names', [])
           for offset, name in enumerate(slot_names):
               mapping[name] = Member(name, clsname, offset)
           return type.__new__(mcls, clsname, bases, mapping, **kwargs)

"object.__new__()" メソッドはインスタンス辞書の代わりにスロットを持つ
インスタンスの作成の処理をします。おおざっぱにピュアPythonでのシミュレ
ーションしたコードがこちらです。

   class Object:
       'Simulate how object.__new__() allocates memory for __slots__'

       def __new__(cls, *args, **kwargs):
           'Emulate object_new() in Objects/typeobject.c'
           inst = super().__new__(cls)
           if hasattr(cls, 'slot_names'):
               empty_slots = [null] * len(cls.slot_names)
               object.__setattr__(inst, '_slotvalues', empty_slots)
           return inst

       def __setattr__(self, name, value):
           'Emulate _PyObject_GenericSetAttrWithDict() Objects/object.c'
           cls = type(self)
           if hasattr(cls, 'slot_names') and name not in cls.slot_names:
               raise AttributeError(
                   f'{cls.__name__!r} object has no attribute {name!r}'
               )
           super().__setattr__(name, value)

       def __delattr__(self, name):
           'Emulate _PyObject_GenericSetAttrWithDict() Objects/object.c'
           cls = type(self)
           if hasattr(cls, 'slot_names') and name not in cls.slot_names:
               raise AttributeError(
                   f'{cls.__name__!r} object has no attribute {name!r}'
               )
           super().__delattr__(name)

実際のクラスでシミュレーションコードを利用するには、 "Object" を継承し
、 *metaclass* を "Type" にします。

   class H(Object, metaclass=Type):
       'Instance variables stored in slots'

       slot_names = ['x', 'y']

       def __init__(self, x, y):
           self.x = x
           self.y = y

この時点ではメタクラスは *x* と *y* のメンバーオブジェクトをロードしま
す。

   >>> from pprint import pp
   >>> pp(dict(vars(H)))
   {'__module__': '__main__',
    '__doc__': 'Instance variables stored in slots',
    'slot_names': ['x', 'y'],
    '__init__': <function H.__init__ at 0x7fb5d302f9d0>,
    'x': <Member 'x' of 'H'>,
    'y': <Member 'y' of 'H'>}

インスタンスが作成されると、属性が保持される "slot_values"  のリストを
インスタンスが保持します:

   >>> h = H(10, 20)
   >>> vars(h)
   {'_slotvalues': [10, 20]}
   >>> h.x = 55
   >>> vars(h)
   {'_slotvalues': [55, 20]}

スペルミスや、未アサインの属性は例外を送出します。

   >>> h.xz
   Traceback (most recent call last):
       ...
   AttributeError: 'H' object has no attribute 'xz'
