描述器 (Descriptor) 指南
************************

作者:
   Raymond Hettinger

聯絡方式:
   <python at rcn dot com>


目錄
^^^^

* 描述器 (Descriptor) 指南

  * 入門

    * 簡單範例：回傳常數的描述器

    * 動態查找

    * 受管理的屬性

    * 自訂名稱

    * 結語

  * 完整實用範例

    * 驗證器類別

    * 自訂驗證器

    * 實際應用

  * 技術教學

    * 摘要

    * 定義與介紹

    * 描述器協定

    * 描述器呼叫概觀

    * 從實例呼叫

    * 從類別呼叫

    * Invocation from super

    * Summary of invocation logic

    * Automatic name notification

    * ORM 範例

  * Pure Python Equivalents

    * Properties

    * Functions and methods

    * Kinds of methods

    * Static methods

    * Class methods

    * Member objects and __slots__

*描述器*讓物件自訂屬性能夠被查找、儲存和刪除。

此指南有四個主要章節：

1. 「入門」提供基本概述，從簡單範例開始逐步介紹，一次新增一個功能。如
   果你是描述器的新手，請從這裡開始。

2. 第二個章節展示完整、實用的描述器範例。如果你已經了解基礎知識，可以
   從那裡開始。

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                       # 一般類別屬性
       y = Ten()                   # 描述器實例

互動式工作階段顯示一般屬性查找和描述器查找之間的差異：

   >>> a = A()                     # 建立類別 A 的實例
   >>> a.x                         # 一般屬性查找
   5
   >>> a.y                         # 描述器查找
   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()              # 描述器實例

       def __init__(self, dirname):
           self.dirname = dirname          # 一般實例屬性

互動式工作階段顯示查找是動態的——每次都會計算不同的更新答案：

   >>> s = Directory('songs')
   >>> g = Directory('games')
   >>> s.size                              # songs 目錄有二十個檔案
   20
   >>> g.size                              # games 目錄有三個檔案
   3
   >>> os.remove('games/chess')            # 刪除一個遊戲
   >>> g.size                              # 檔案數量自動更新
   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()             # 描述器實例

       def __init__(self, name, age):
           self.name = name                # 一般實例屬性
           self.age = age                  # 呼叫 __set__()

       def birthday(self):
           self.age += 1                   # 同時呼叫 __get__() 和 __set__()

互動式工作階段顯示對受管理屬性 *age* 的所有存取都被記錄，但一般屬性
*name* 則不會被記錄：

   >>> mary = Person('Mary M', 30)         # 初始的 age 更新被記錄
   INFO:root:Updating 'age' to 30
   >>> dave = Person('David D', 40)
   INFO:root:Updating 'age' to 40

   >>> vars(mary)                          # 實際資料在私有屬性中
   {'name': 'Mary M', '_age': 30}
   >>> vars(dave)
   {'name': 'David D', '_age': 40}

   >>> mary.age                            # 存取資料並記錄查找
   INFO:root:Accessing 'age' giving 30
   30
   >>> mary.birthday()                     # 更新也會被記錄
   INFO:root:Accessing 'age' giving 30
   INFO:root:Updating 'age' to 31

   >>> dave.name                           # 一般屬性查找不會被記錄
   'David D'
   >>> dave.age                            # 只有受管理的屬性會被記錄
   INFO:root:Accessing 'age' giving 40
   40

這個範例的一個主要問題是私有名稱 *_age* 在 *LoggedAgeAccess* 類別中是
硬編碼的。這意味著每個實例只能有一個被記錄的屬性，而且它的名稱是不可變
的。在下一個範例中，我們將修正這個問題。


自訂名稱
--------

當類別使用描述器時，它可以告知每個描述器使用了哪個變數名稱。

在這個範例中，"Person" 類別有兩個描述器實例：*name* 和 *age*。當定義
"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()                # 第一個描述器實例
       age = LoggedAccess()                 # 第二個描述器實例

       def __init__(self, name, age):
           self.name = name                 # 呼叫第一個描述器
           self.age = age                   # 呼叫第二個描述器

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

An interactive session shows that the "Person" class has called
"__set_name__()" so that the field names would be recorded.  Here we
call "vars()" to look up the descriptor without triggering it:

   >>> 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

兩個 *Person* 實例只包含私有名稱：

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


結語
----

*描述器* 是我們對任何定義了 "__get__()"、"__set__()" 或 "__delete__()"
的物件的稱呼。

描述器可以選擇性地擁有 "__set_name__()" 方法。這只在描述器需要知道建立
它的類別或被指派的類別變數名稱時使用。（此方法如果存在，即使類別不是描
述器也會被呼叫。）

描述器在屬性查找期間由點運算子呼叫。如果描述器是透過
"vars(some_class)[descriptor_name]" 間接存取，會回傳描述器實例而不呼叫
它。

描述器只有在用作類別變數時才能運作。當放在實例中時，它們沒有效果。

描述器的主要動機是提供一個掛鉤，讓儲存在類別變數中的物件能夠控制屬性查
找期間發生的事情。

傳統上，呼叫的類別控制查找期間發生的事情。描述器顛倒了這種關係，讓被查
找的資料在這件事上有發言權。

描述器在整個語言中被廣泛使用。函式就是透過這種方式轉變為綁定方法。常見
工具如 "classmethod()"、"staticmethod()"、"property()" 和
"functools.cached_property()" 都是以描述器實作的。


完整實用範例
============

在這個範例中，我們建立一個實用且強大的工具，用於找出惡名昭彰且難以發現
的資料損壞錯誤。


驗證器類別
----------

驗證器是用於受管理屬性存取的描述器。在儲存任何資料之前，它會驗證新數值
是否符合各種型別和範圍限制。如果不符合這些限制，它會引發例外以防止資料
在源頭被損壞。

這個 "Validator" 類別既是 *抽象基底類別* 也是受管理屬性描述器：

   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()" 方法來依需
要測試各種限制。


自訂驗證器
----------

以下是三個實用的資料驗證工具：

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 a 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)      # 被阻止：'Widget' 不是全大寫
   Traceback (most recent call last):
       ...
   ValueError: Expected <method 'isupper' of 'str' objects> to be true for 'Widget'

   >>> Component('WIDGET', 'metle', 5)      # 被阻止：'metle' 拼寫錯誤
   Traceback (most recent call last):
       ...
   ValueError: Expected 'metle' to be one of {'metal', 'plastic', 'wood'}

   >>> Component('WIDGET', 'metal', -5)     # 被阻止：-5 是負數
   Traceback (most recent call last):
       ...
   ValueError: Expected -5 to be at least 0

   >>> Component('WIDGET', 'metal', 'V')    # 被阻止：'V' 不是數字
   Traceback (most recent call last):
       ...
   TypeError: Expected 'V' to be an int or float

   >>> c = Component('WIDGET', 'metal', 5)  # 允許：輸入有效


技術教學
========

接下來是關於描述器運作機制和細節的更技術性教學。


摘要
----

定義描述器、總結協定，並展示描述器如何被呼叫。提供展示物件關聯映射如何
運作的範例。

學習描述器不僅提供更大的工具集，還能更深入理解 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)" 呼
叫。

But it is more common for a descriptor to be invoked automatically
from attribute access.

The expression "obj.x" looks up the attribute "x" in the chain of
namespaces for "obj".  If the search finds a descriptor outside of the
instance "__dict__", its "__get__()" method is invoked according to
the precedence rules listed below.

The details of invocation depend on whether "obj" is an object, class,
or instance of 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 = sentinel('null')
       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)

Note, there is no "__getattr__()" hook in the "__getattribute__()"
code.  That is why calling "__getattribute__()" directly or with
"super().__getattribute__" will bypass "__getattr__()" entirely.

Instead, it is the dot operator and the "getattr()" function that are
responsible for invoking "__getattr__()" whenever "__getattribute__()"
raises an "AttributeError".  Their logic is encapsulated in a helper
function:

   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__


從類別呼叫
----------

The logic for a dotted lookup such as "A.x" is in
"type.__getattribute__()".  The steps are similar to those for
"object.__getattribute__()" but the instance dictionary lookup is
replaced by a search through the class's *method resolution order*.

如果找到描述器，則使用 "desc.__get__(None, A)" 呼叫它。

The full C implementation can be found in "type_getattro()" and
"_PyType_Lookup()" in Objects/typeobject.c.


Invocation from super
---------------------

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

A dotted lookup such as "super(A, obj).m" searches
"obj.__class__.__mro__" for the base class "B" immediately following
"A" and then returns "B.__dict__['m'].__get__(obj, A)".  If not a
descriptor, "m" is returned unchanged.

The full C implementation can be found in "super_getattro()" in
Objects/typeobject.c.  A pure Python equivalent can be found in
Guido's Tutorial.


Summary of invocation logic
---------------------------

The mechanism for descriptors is embedded in the "__getattribute__()"
methods for "object", "type", and "super()".

要記住的重點是：

* Descriptors are invoked by the "__getattribute__()" method.

* Classes inherit this machinery from "object", "type", or "super()".

* Overriding "__getattribute__()" prevents automatic descriptor calls
  because all the descriptor logic is in that method.

* "object.__getattribute__()" and "type.__getattribute__()" make
  different calls to "__get__()".  The first includes the instance and
  may include the class.  The second puts in "None" for the instance
  and always includes the class.

* Data descriptors always override instance dictionaries.

* Non-data descriptors may be overridden by instance dictionaries.


Automatic name notification
---------------------------

Sometimes it is desirable for a descriptor to know what class variable
name it was assigned to.  When a new class is created, the "type"
metaclass scans the dictionary of the new class.  If any of the
entries are descriptors and if they define "__set_name__()", that
method is called with two arguments.  The *owner* is the class where
the descriptor is used, and the *name* is the class variable the
descriptor was assigned to.

The implementation details are in "type_new()" and "set_names()" in
Objects/typeobject.c.

Since the update logic is in "type.__new__()", notifications only take
place at the time of class creation.  If descriptors are added to the
class afterwards, "__set_name__()" will need to be called manually.


ORM 範例
--------

The following code is a simplified skeleton showing how data
descriptors could be used to implement an object relational mapping.

The essential idea is that the data is stored in an external database.
The Python instances only hold keys to the database's tables.
Descriptors take care of lookups or updates:

   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()

We can use the "Field" class to define models that describe the schema
for each table in a database:

   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

To use the models, first connect to the database:

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

An interactive session shows how data is retrieved from the database
and how it can be updated:

   >>> 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'


Pure Python Equivalents
=======================

The descriptor protocol is simple and offers exciting possibilities.
Several use cases are so common that they have been prepackaged into
built-in tools. Properties, bound methods, static methods, class
methods, and __slots__ are all based on the descriptor protocol.


Properties
----------

Calling "property()" is a succinct way of building a data descriptor
that triggers a function call upon access to an attribute.  Its
signature is:

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

The documentation shows a typical use to define a managed attribute
"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.")

To see how "property()" is implemented in terms of the descriptor
protocol, here is a pure Python equivalent that implements most of the
core functionality:

   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

       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
           return self.fget(obj)

       def __set__(self, obj, value):
           if self.fset is None:
               raise AttributeError
           self.fset(obj, value)

       def __delete__(self, obj):
           if self.fdel is None:
               raise AttributeError
           self.fdel(obj)

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

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

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

The "property()" builtin helps whenever a user interface has granted
attribute access and then subsequent changes require the intervention
of a method.

For instance, a spreadsheet class may grant access to a cell value
through "Cell('b10').value". Subsequent improvements to the program
require the cell to be recalculated on every access; however, the
programmer does not want to affect existing client code accessing the
attribute directly.  The solution is to wrap access to the value
attribute in a property data descriptor:

   class Cell:
       ...

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

Either the built-in "property()" or our "Property()" equivalent would
work in this example.


Functions and methods
---------------------

Python's object oriented features are built upon a function based
environment. Using non-data descriptors, the two are merged
seamlessly.

Functions stored in class dictionaries get turned into methods when
invoked. Methods only differ from regular functions in that the object
instance is prepended to the other arguments.  By convention, the
instance is called *self* but could be called *this* or any other
variable name.

Methods can be created manually with "types.MethodType" which is
roughly equivalent to:

   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)

       def __getattribute__(self, name):
           "Emulate method_getset() in Objects/classobject.c"
           if name == '__doc__':
               return self.__func__.__doc__
           return object.__getattribute__(self, name)

       def __getattr__(self, name):
           "Emulate method_getattro() in Objects/classobject.c"
           return getattr(self.__func__, name)

       def __get__(self, obj, objtype=None):
           "Emulate method_descr_get() in Objects/classobject.c"
           return self

To support automatic creation of methods, functions include the
"__get__()" method for binding methods during attribute access.  This
means that functions are non-data descriptors that return bound
methods during dotted lookup from an instance.  Here's how it works:

   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)

Running the following class in the interpreter shows how the function
descriptor works in practice:

   class D:
       def f(self):
            return self

   class D2:
       pass

The function has a *qualified name* attribute to support
introspection:

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

Accessing the function through the class dictionary does not invoke
"__get__()".  Instead, it just returns the underlying function object:

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

Dotted access from a class calls "__get__()" which just returns the
underlying function unchanged:

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

The interesting behavior occurs during dotted access from an instance.
The dotted lookup calls "__get__()" which returns a bound method
object:

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

Internally, the bound method stores the underlying function and the
bound instance:

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

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

If you have ever wondered where *self* comes from in regular methods
or where *cls* comes from in class methods, this is it!


Kinds of methods
----------------

Non-data descriptors provide a simple mechanism for variations on the
usual patterns of binding functions into methods.

To recap, functions have a "__get__()" method so that they can be
converted to a method when accessed as attributes.  The non-data
descriptor transforms an "obj.f(*args)" call into "f(obj, *args)".
Calling "cls.f(*args)" becomes "f(*args)".

This chart summarizes the binding and its two most useful variants:

   +-------------------+------------------------+--------------------+
   | Transformation    | Called from an object  | Called from a      |
   |                   |                        | class              |
   |===================|========================|====================|
   | 函式              | f(obj, *args)          | f(*args)           |
   +-------------------+------------------------+--------------------+
   | staticmethod      | f(*args)               | f(*args)           |
   +-------------------+------------------------+--------------------+
   | classmethod       | f(type(obj), *args)    | f(cls, *args)      |
   +-------------------+------------------------+--------------------+


Static methods
--------------

Static methods return the underlying function without changes.
Calling either "c.f" or "C.f" is the equivalent of a direct lookup
into "object.__getattribute__(c, "f")" or "object.__getattribute__(C,
"f")". As a result, the function becomes identically accessible from
either an object or a class.

Good candidates for static methods are methods that do not reference
the "self" variable.

For instance, a statistics package may include a container class for
experimental data.  The class provides normal methods for computing
the average, mean, median, and other descriptive statistics that
depend on the data. However, there may be useful functions which are
conceptually related but do not depend on the data.  For instance,
"erf(x)" is handy conversion routine that comes up in statistical work
but does not directly depend on a particular dataset. It can be called
either from an object or the class:  "s.erf(1.5) --> 0.9332" or
"Sample.erf(1.5) --> 0.9332".

Since static methods return the underlying function with no changes,
the example calls are unexciting:

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

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

Using the non-data descriptor protocol, a pure Python version of
"staticmethod()" would look like this:

   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)

       @property
       def __annotations__(self):
           return self.f.__annotations__

The "functools.update_wrapper()" call adds a "__wrapped__" attribute
that refers to the underlying function.  Also it carries forward the
attributes necessary to make the wrapper look like the wrapped
function, including "__name__", "__qualname__", and "__doc__".


Class methods
-------------

Unlike static methods, class methods prepend the class reference to
the argument list before calling the function.  This format is the
same for whether the caller is an object or a class:

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

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

This behavior is useful whenever the method only needs to have a class
reference and does not rely on data stored in a specific instance.
One use for class methods is to create alternate class constructors.
For example, the classmethod "dict.fromkeys()" creates a new
dictionary from a list of keys.  The pure Python equivalent is:

   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

Now a new dictionary of unique keys can be constructed like this:

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

Using the non-data descriptor protocol, a pure Python version of
"classmethod()" would look like this:

   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)
           return MethodType(self.f, cls)

The "functools.update_wrapper()" call in "ClassMethod" adds a
"__wrapped__" attribute that refers to the underlying function.  Also
it carries forward the attributes necessary to make the wrapper look
like the wrapped function: "__name__", "__qualname__", "__doc__", and
"__annotations__".


Member objects and __slots__
----------------------------

When a class defines "__slots__", it replaces instance dictionaries
with a fixed-length array of slot values.  From a user point of view
that has several effects:

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.

It is not possible to create an exact drop-in pure Python version of
"__slots__" because it requires direct access to C structures and
control over object memory allocation.  However, we can build a mostly
faithful simulation where the actual C structure for slots is emulated
by a private "_slotvalues" list.  Reads and writes to that private
structure are managed by member descriptors:

   null = sentinel('null')

   class Member:

       def __init__(self, name, clsname, offset):
           'Emulate PyMemberDef in Include/descrobject.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}>'

The "type.__new__()" method takes care of adding member objects to
class variables:

   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)

The "object.__new__()" method takes care of creating instances that
have slots instead of an instance dictionary.  Here is a rough
simulation in pure 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)

To use the simulation in a real class, just inherit from "Object" and
set the *metaclass* to "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

At this point, the metaclass has loaded member objects for *x* and
*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'>}

When instances are created, they have a "slot_values" list where the
attributes are stored:

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

Misspelled or unassigned attributes will raise an exception:

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