Python 描述符對象 Descriptor Objects

Reproduce from

在 Python 眾多原生特性中,描述符可能是最好被自定義的特性之一,但它在底層實現的方法和屬性卻無時不刻被使用著,它優雅的實現方式體現出 Python 簡潔之美。

簡介

Python 描述符是一種創建對象屬性的方法。描述符具有諸多優點,諸如:保護屬性不受修改,屬性類型檢查,和自動更新某個依賴屬性的值等。

定義

  • 一個描述符是一個有綁定屬性的對象屬性,它的訪問控制會被描述器協議方法重寫。(In general, a descriptor is an object attribute with “binding behavior”, one whose attribute access has been overridden by methods in the descriptor protocol. )
  • 描述符協議是以下方法:__get__()__set__(),和 __delete__()。如果一個類定義了任意這些方法,那么這個類就是描述符。(Those methods are __get__(), __set__(), and __delete__(). If any of those methods are defined for an object, it is said to be a descriptor.)
  • 屬性訪問的默認行為是從對象的字典(object's dictionary)中獲取,獲取(get),設置(set),或刪除(delete)屬性。例如,當訪問 a.x 時,有一個查找鏈,開始是 a.__dict__['x'],接著 type(a).__dict__['x'],之后繼續在基類中尋找,不包括元類 metaclasses。如果查找的目標屬性是一個定義了描述符的類對象,則 Python 會用描述符的方法覆蓋默認行為。這種情況發生在在優先級鏈中發生的時機取決于描述符的哪些方法被定義。(If the looked-up value is an object defining one of the descriptor methods, then Python may override the default behavior and invoke the descriptor method instead. Where this occurs in the precedence chain depends on which descriptor methods were defined.)
  • 描述符是一種功能強大的通用協議。它是 @propertiesmethods(方法),@staticmethod(靜態方法),@classmethod(類方法),和 super() 背后的機制。(Descriptors are a powerful, general purpose protocol. They are the mechanism behind properties, methods, static methods, class methods, and super().)

描述符協議

  • __get__(self, instance, owner)
    def __get__(self, instance, owner):
    '''
      :param self: 描述符對象本身
      :param instance: 使用描述符的對象的實例
      :param owner: 使用描述符的對象擁有者
    '''
    
  • __set__(self, instance, value)
    def __set__(self, instance, value):
    '''
      :param value: 對描述符的賦值
    '''
    
  • __delete__(self, instance)

實例

class Descriptor:
    def __init__(self, name):
        self._name = name

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

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

    def __delete__(self, instance):
        del self._name

class Person:
    age = Descriptor('age')

為什么需要描述符

Python 是一個動態類型解釋性語言,不像 C / Java 等靜態編譯型語言,數據類型在編譯時便可以進行驗證,而 Python 中必須添加額外的類型檢查邏輯代碼才能做到這一點。

假設我們有這樣一個類:

class Movie:
    def __init__( self, 
            title, 
            description, 
            score, 
            ticket):
        self.title = title
        self.description = description
        self.score = score
        self.ticket = ticket

這里,電影的分數不能是負分,這個是錯誤行為,希望 Movie 類可以預防這個問題。

class Movie:
    def __init__(self, 
            title, 
            description, 
            score, 
            ticket):
        self.title = title
        self.description = description
     self.ticket = ticket
        if score < 0:
            raise ValueError("Negative value not allowed:{}".format(score))
        self.score = score

這樣修改可以防止初始化對象的時候給電影打負分,但是如果對于已經存在的類實例就無能為力了。如果有人試著運行 movie.score = -1 ,那么誰也沒法阻止。

Getter & Setter

實現對于 scoregetter()setter() 方法來防止 score 小于 0。

class Movie:
    def __init__(self, 
            title, 
            description, 
            score, 
            ticket):
        self.title = title
        self.description = description
     self.ticket = ticket
        if score < 0:
            raise ValueError("Negative value not allowed:{}".format(score))
        self._score = score

    def set_score(self, score):
        if score >= 0:
            self._score = score
        else:
            self._score = 0

    def get_score(self):
        return self._score

但是,大量的 getter() 和 setter() 會導致類型定義的臃腫和邏輯混亂。從 OOP 思想來看,只有屬性自己最清楚自己的類型,而不是他所在的類,因此如果能將類型檢查的邏輯根植于屬性內部,那么就可以解決這個問題 -- @property

Property

注意,這里 self._score 才是對象的真正的屬性,而 type(Movie.score)Property。每次調用 object.score 實際就是在調用 Property 相應的 getter()setter(),或是 deleter()。如果在 setter() 中也寫的是 self.score = score,則是自己調用自己,陷入不斷的遞歸中。

class Movie:
    def __init__(self, ticket, score):
        self.score = score
        self.ticket = ticket

    @property
    def score(self):
        return self._score

    @score.setter
    def score(self, score):
        if score < 0:
            raise ValueError("Negative value not allowed:{}".format(score))
        self._score = score

    @score.deleter
    def score(self):
        raise AttributeError("Can not delete score")

Property 的不足

對于 Property 來說,最大的不足就是它們不能重復使用。如果有多個屬性需要寫為 Property,那么代碼 / 重復的邏輯便會出現不少。雖然 Property 可以讓類從外部看起來借口整潔漂亮,但是卻做不到內部同樣整潔漂亮。

Descriptor

如何用描述符來解決上面 Property 邏輯重復的問題。

如果一個實例同時定義了 __get__()__set__(),那就被認為是數據描述符。如果描述符只定義了 __get__() 就被稱為非數據描述符。If an object defines both __get__() and __set__(), it is considered a data descriptor. Descriptors that only define __get__() are called non-data descriptors (they are typically used for methods but other uses are possible).

數據描述符和非數據描述符不同在于「對于實例字典(dictionary)中的 items/entries 的計算的覆蓋(override)」。Data and non-data descriptors differ in how overrides are calculated with respect to entries in an instance’s dictionary.

如果實例的字典有一個和數據描述符一樣名稱的 item/entry,數據描述符優先。如果實例的字典有一個和非數據描述符一樣名稱的 item/entry,字典中的 item/entry 優先。
If an instance’s dictionary has an entry with the same name as a data descriptor, the data descriptor takes precedence. If an instance’s dictionary has an entry with the same name as a non-data descriptor, the dictionary entry takes precedence.

class Integer:
    def __init__(self, name):
        print ('descriptor __init__')
        self.name = name
    
    def __get__(self, instance, owner):
        print ('descriptor __get__')
        return instance.__dict__[self.name]

    def __set__(self, instance, value):
        print ('descriptor __set__')
        if value < 0:
           raise ValueError("Negative value not allowed")
        instance.__dict__[self.name] = value
>>> class Movie:
...     # class attribute
...     score = Integer('score')
...     ticket = Integer('ticket')
...
...     def __init__(self):
...         pass
descriptor __init__
descriptor __init__
>>> movie = Movie()
>>> movie.__dict__['ticket']
KeyError: 'ticket'
>>> movie.ticket = 1
descriptor __set__
>>> movie.ticket
descriptor __get__
1
>>> movie.__dict__['ticket']
1

在調用 movie.ticket = 1 時,descriptor 的 __set__() 使用 instance.__dict__[self.name] = value 在 Movie instance 中添加了新的 attribute 并且賦值。

但是這樣有些生硬,所以還缺一個構造函數。

class Integer:
    def __init__(self, name):
        print ('descriptor __init__')
        self.name = name
    
    def __get__(self, instance, owner):
        print ('descriptor __get__')
        return instance.__dict__[self.name]

    def __set__(self, instance, value):
        print ('descriptor __set__')
        if value < 0:
           raise ValueError("Negative value not allowed")
        instance.__dict__[self.name] = value


class Movie:
    # class attribute
    score = Integer('score')
    ticket = Integer('ticket')
    
    def __init__(self, score, ticket):
        # using self.attr to convert class attribute to object attribute
        # and call descriptor __set__()
        self.score = score
        self.ticket = ticket

這樣在 get,set,和 delete 屬性的時候都會進入的 Integer__get____set__,和 __del__ 從而減少了重復的邏輯。

那么 Class 的屬性是怎么變為了 instance 的屬性呢?在 __init__ 函數里訪問的是自己的 self.scoreself.ticket,怎么和類屬性 socre,ticket 關聯起來的?它們的調用順序是怎樣的?

Invoking Descriptors 調用描述符

這里我將翻譯 Python Descriptor 官方文檔,因為結合 MRO,和 Python 魔法方法,這段講解的已經比較詳細了。

描述符可以通過它的名字被直接調用。例如 d.__get__(obj)Movie.__dict__['ticket'].__get__(m, None)。(A descriptor can be called directly by its method name. For example, d.__get__(obj).)

另外,一般的,描述符的調用自動作為屬性調用。例如,obj.dobj 的字典里查找 d,如果 d 定義了 __get__() 方法,那 d.__get__(obj) 就會根據優先原則被調用。(Alternatively, it is more common for a descriptor to be invoked automatically upon attribute access. For example, obj.d looks up d in the dictionary of obj. If d defines the method __get__(), then d.__get__(obj) is invoked according to the precedence rules listed below.)

調用細節取決于 obj 是實例還是類。(The details of invocation depend on whether obj is an object or a class.)

Python 魔法方法指南
先復習一下 Python 的魔法方法

  • __getattribute__(self, name)
    __getattribute__ 只能用新式類。當 obj.attr 訪問實例屬性時,實際調用的是 __getattribute__
  • __getattr__(self, name)
    當訪問一個根本不存在的(或者暫時不存在)屬性時,__getattr__(self, name) 會被調用。
  • __call__(self, [args...])
    當調用一個類時,例如 obj = MyClass(),實際就是調用 MyClass.__call__()

對于實例,機制 object.__getattribute__() 中,將 b.x 的調用轉換為 type(b).__dict__['x'].__get__(b, type(b))。這個實現是通過優先鏈給予數據描述符比實例的變量更高的優先級,實例的變量的優先級高于非數據描述符,而__getattr__() 的優先級最低。(For objects, the machinery is in object.__getattribute__() which transforms b.x into type(b).__dict__['x'].__get__(b, type(b)). The implementation works through a precedence chain that gives data descriptors priority over instance variables, instance variables priority over non-data descriptors, and assigns lowest priority to __getattr__() if provided. The full C implementation can be found in PyObject_GenericGetAttr() in Objects/object.c.)

對于類,機制在 type.__getattribute__() 中,將 B.x 轉換為 B.__dict__['x'].__get__(None, B)。(For classes, the machinery is in type.__getattribute__() which transforms B.x into B.__dict__['x'].__get__(None, B).)

def __getattribute__(self, key):
    "Emulate type_getattro() in Objects/typeobject.c"
    v = object.__getattribute__(self, key)
    if hasattr(v, '__get__'):
        return v.__get__(None, self)
    return v
  • 描述符被 __getattribute__() 方法調用(descriptors are invoked by the __getattribute__() method)
  • 覆寫 __getattribute__() 可以阻止自動的描述符調用(overriding __getattribute__() prevents automatic descriptor calls)
  • object.__getattribute__()type.__getattribute__() 對于 __get__() 的調用不同(object.__getattribute__() and type.__getattribute__() make different calls to __get__().)
  • 數據描述符會覆蓋實例字典(data descriptors always override instance dictionaries.)
  • 非數據描述符會被實例字典覆蓋。(non-data descriptors may be overridden by instance dictionaries.)

super() 返回的 object 也有 __getattribute__() 方法用來調用描述符。調用 super(B, obj).m() 會使用 obj.__class__.__mro__ 查找類 B 的基類 A,并返回 A.__dict__['m'].__get__(obj, B)。如果返回的不是描述符,m 返回的就是無變化的(類 A 的變量)。如果不在類 A 的字典中,m 恢復使用 object.__getattribute__() 來搜索。(The object returned by super() also has a custom __getattribute__() method for invoking descriptors. The call super(B, obj).m() searches obj.__class__.__mro__ for the base class A immediately following B and then returns A.__dict__['m'].__get__(obj, B). If not a descriptor, m is returned unchanged. If not in the dictionary, m reverts to a search using object.__getattribute__().)

以上的細節展示了描述符被調用的機制在 object,type,和 super() 中的 __getattribute__() 被實現。來源于 object 的類會繼承這個機制或元類提供了相似的功能。另外,類可以禁止描述符的調用通過覆寫 __getattribute__()。(The details above show that the mechanism for descriptors is embedded in the __getattribute__() methods for object, type, and super(). Classes inherit this machinery when they derive from object or if they have a meta-class providing similar functionality. Likewise, classes can turn-off descriptor invocation by overriding __getattribute__().)

  1. 無論是實例還是類,實際都是在 type.__dict__['x'] 找 descriptor;
  2. 內部實際是按照 MRO 順序,順著類,父母類一路找,直到找到 descriptor;
  3. 找到后,判斷是否是 data descriptor;
  4. 如果不是 data descriptor, 在查找實例的 dict;
  5. 如果實例的 dict 沒有,則嘗試調用 descriptor 的 __get__()
  6. 調用不成功,調用 __getattr__() 進行錯誤處理。

源碼分析

通過 CPython 源碼,可以驗證之前官方文檔中的說明。

PyObject_GenericGetAttr

PyObject *
PyObject_GenericGetAttr(PyObject *obj, PyObject *name)
{
    return _PyObject_GenericGetAttrWithDict(obj, name, NULL, 0);
}

PyObject *
_PyObject_GenericGetAttrWithDict(PyObject *obj, PyObject *name,
                                 PyObject *dict, int suppress)
{
    /* Make sure the logic of _PyObject_GetMethod is in sync with
       this method.
       When suppress=1, this function suppress AttributeError.
    */

    PyTypeObject *tp = Py_TYPE(obj);
    PyObject *descr = NULL;
    PyObject *res = NULL;
    descrgetfunc f;
    Py_ssize_t dictoffset;
    PyObject **dictptr;

    if (!PyUnicode_Check(name)){
        PyErr_Format(PyExc_TypeError,
                     "attribute name must be string, not '%.200s'",
                     name->ob_type->tp_name);
        return NULL;
    }
    Py_INCREF(name);

    if (tp->tp_dict == NULL) {
        if (PyType_Ready(tp) < 0)
            goto done;
    }

    descr = _PyType_Lookup(tp, name);

    f = NULL;
    if (descr != NULL) {
        Py_INCREF(descr);
        f = descr->ob_type->tp_descr_get;
        if (f != NULL && PyDescr_IsData(descr)) {
            res = f(descr, obj, (PyObject *)obj->ob_type);
            if (res == NULL && suppress &&
                    PyErr_ExceptionMatches(PyExc_AttributeError)) {
                PyErr_Clear();
            }
            goto done;
        }
    }

    if (dict == NULL) {
        /* Inline _PyObject_GetDictPtr */
        dictoffset = tp->tp_dictoffset;
        if (dictoffset != 0) {
            if (dictoffset < 0) {
                Py_ssize_t tsize;
                size_t size;

                tsize = ((PyVarObject *)obj)->ob_size;
                if (tsize < 0)
                    tsize = -tsize;
                size = _PyObject_VAR_SIZE(tp, tsize);
                _PyObject_ASSERT(obj, size <= PY_SSIZE_T_MAX);

                dictoffset += (Py_ssize_t)size;
                _PyObject_ASSERT(obj, dictoffset > 0);
                _PyObject_ASSERT(obj, dictoffset % SIZEOF_VOID_P == 0);
            }
            dictptr = (PyObject **) ((char *)obj + dictoffset);
            dict = *dictptr;
        }
    }
    if (dict != NULL) {
        Py_INCREF(dict);
        res = PyDict_GetItemWithError(dict, name);
        if (res != NULL) {
            Py_INCREF(res);
            Py_DECREF(dict);
            goto done;
        }
        else {
            Py_DECREF(dict);
            if (PyErr_Occurred()) {
                if (suppress && PyErr_ExceptionMatches(PyExc_AttributeError)) {
                    PyErr_Clear();
                }
                else {
                    goto done;
                }
            }
        }
    }

    if (f != NULL) {
        res = f(descr, obj, (PyObject *)Py_TYPE(obj));
        if (res == NULL && suppress &&
                PyErr_ExceptionMatches(PyExc_AttributeError)) {
            PyErr_Clear();
        }
        goto done;
    }

    if (descr != NULL) {
        res = descr;
        descr = NULL;
        goto done;
    }

    if (!suppress) {
        PyErr_Format(PyExc_AttributeError,
                     "'%.50s' object has no attribute '%U'",
                     tp->tp_name, name);
    }
  done:
    Py_XDECREF(descr);
    Py_DECREF(name);
    return res;
}

通過分析上面的源碼可以看到:

  • PyTypeObject *tp = Py_TYPE(obj); 填充了 tp_dict
  • 之后 descr = _PyType_Lookup(tp, name); 找到 descriptor;
  • descr->ob_type->tp_descr_get != NULLPyDescr_IsData(descr) 判斷數據描述符
  • 不是數據描述符,且 dict != NULL,返回 PyDict_GetItemWithError(dict, name)
  • dict 中沒有,descr->ob_type->tp_descr_get 返回非數據描述符的 __get__() 方法。

_PyType_Lookup

/* Internal API to look for a name through the MRO.
   This returns a borrowed reference, and doesn't set an exception! */
PyObject *
_PyType_Lookup(PyTypeObject *type, PyObject *name)
{
    PyObject *res;
    int error;
    unsigned int h;
    
    ############
    # 緩存部分代碼
    ############

    /* We may end up clearing live exceptions below, so make sure it's ours. */
    assert(!PyErr_Occurred());

    res = find_name_in_mro(type, name, &error);

    ############
    # 剩余代碼
    ############
}

可以看到之前的 descr = _PyType_Lookup(tp, name); 是來自于 find_name_in_mro(type, name, &error);,descriptor 是根據 MRO 順序從類 / 父母類中找到的。

find_name_in_mro

/* Internal API to look for a name through the MRO, bypassing the method cache.
   This returns a borrowed reference, and might set an exception.
   'error' is set to: -1: error with exception; 1: error without exception; 0: ok */
static PyObject *
find_name_in_mro(PyTypeObject *type, PyObject *name, int *error)
{
    Py_ssize_t i, n;
    PyObject *mro, *res, *base, *dict;
    Py_hash_t hash;

    ############
    # 代碼
    ############

    /* Look in tp_dict of types in MRO */
    mro = type->tp_mro;

    if (mro == NULL) {
        if ((type->tp_flags & Py_TPFLAGS_READYING) == 0) {
            if (PyType_Ready(type) < 0) {
                *error = -1;
                return NULL;
            }
            mro = type->tp_mro;
        }
        if (mro == NULL) {
            *error = 1;
            return NULL;
        }
    }

    ############
    # 剩余代碼
    ############
}

Look in tp_dict of types in MRO,mro = type->tp_mro; 每個類都會有一個 tp_mro,通過這個確定遍歷的順序。

代碼的運行映證了上面文檔描述的調用描述符順序。

Property

class Property(fget=None, fset=None, fdel=None, doc=None)

這時我們再回來看比較常用的 Property。

Calling Property() 是一個構建數據描述符的簡單的方法,該數據描述符在訪問屬性時觸發函數調用。(Calling Property() is a succinct way of building a data descriptor that triggers function calls upon access to an attribute.)

Property 有兩種使用方式,一種是函數模式,一種是裝飾器模式。

函數模式

class C:
    def __init__(self):
        self._x = None
    
    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(),首先定義的 class 必須是新式類(object 的子類),Python 3 只有新式類。如果 c 是 C 的實例,c.x 將調用 fget() 在這里就是 getx()c.x = value 將調用 fset() 在這里就是 setx()del c.x 將調用 fdel() 在這里就是 delx()

使用 Property 的好處就是因為在訪問屬性的時候可以做一些檢查。如果沒有嚴格的要求,直接使用實例屬性可能更方便。

裝飾器模式

class C:
    def __init__(self):
        self._x = None

    @property
    def x(self):
        return self._x

    @x.setter
    def x(self, value):
        self._x = value

    @x.deleter
    def x(self):
        del self._x

注意:三個函數的名字(也就是將來要訪問的屬性名)必須一致。

使用 Property 可以非常容易的實現屬性的讀寫控制,如果想要屬性只讀,則只需要提供 getter 方法。

class C:
    def __init__(self):
        self._x = None

    @property
    def x(self):
        return self._x

對于描述符,只實現 get 函數的描述符是非數據描述符,根據屬性查找的優先級,非數據描述符的優先級是可以被實際屬性覆蓋(隱藏)的,但是執行如下代碼:

>>> c = C()
>>> c.x
>>> c.x = 3
Traceback (most recent call last):
  File "<pyshell#39>", line 1, in <module>
    c.x = 3
AttributeError: can't set attribute

從錯誤信息中可以看出,c.x = 3 的時候并不是動態產生一個實例屬性,也就是說 x 并不是被數據描述符,那么原因是什么呢?原因就在 Property,雖然表面上看屬性 x 只設置了 get(),但其實 Property 是一個同時實現了 __get__()__set__()__del__() 方法的類(數據描述符)。因此使用 Property 生成的屬性其實是一個數據描述符

使用 Python 模擬的 Property 代碼如下,可以看到,上面的 "At Property tributeError: can't set attribute” 異常其實是在 Property 中的 __set__() 中引發的,因為用戶沒有設置 fset:

class Property(object):
    "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 __get__(self, obj, objtype=None):
            if obj is None:
                return self
            if self.fget is None:
                raise AttributeError("unreadable attribute")
            return self.fget(obj)

        def __set__(self, obj, value):
            if self.fset is None:
                raise AttributeError("can't set attribute")
            self.fset(obj, value)

        def __delete__(self, obj):
            if self.fdel is None:
                raise AttributeError("can't delete attribute")
            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__)

classmethod 和 staticmethod

classmethod 和 staticmethod 的本質是什么?

class C:
    def __init__(self):
        pass

    @classmethod
    def foo():
        pass

    @staticmethod
    def silly():
        pass

>> C.__dict__
mappingproxy({'__module__': '__main__',
              '__init__': <function __main__.C.__init__(self)>,
              'foo': <classmethod at 0x10ed08da0>,
              'silly': <staticmethod at 0x10ed08dd8>,
              '__dict__': <attribute '__dict__' of 'C' objects>,
              '__weakref__': <attribute '__weakref__' of 'C' objects>,
              '__doc__': None})

可以看到,foo 成為了 classmethod,silly 成為了staticmethod。可以看到這和使用 Property 很像,上面代碼的 type(C.x) 是 property。函數都被重寫了。注意,這里用到了裝飾器方法,因為沒有使用 from functools import wraps,所以函數會被重寫。

由于屬性搜索優先級,所以裝飾器的部分需要寫在 __get__() 中,這樣重寫后的函數 object 才會被自動調用。

這樣看來,classmethod 和 staticmethod 都是描述符,也是裝飾器,即作為裝飾器的描述符(descriptor that can be used as a decorator)。

classmethod

以下部分內容來自《Python Descriptors: Understanding and Using the Descriptor Protocol》,這本書比較有意思,在 AMAZON 上沒有評分,沒有 review。

classmethod 是另外一個作為裝飾器的描述符,但是不像 Property,沒有理由不以裝飾器的方法使用它(property 的使用方式有兩種)。(classmethod is another descriptor that can be used as a decorator, but, unlike property, there's no good reason not to use it as one.)

class classmethod:
    def __init__(self, func):
        self.func = func
    
    def __get__(self, instance, owner):
        return functools.partial(self.func, owner)

classmethod 是被數據描述符,所以他只實現了 __get__()__get__() 方法完全忽略了 instance 參數,因為就像名字寫的 classmethod,方法與類實例無關,只于類本身有關。最好的是,方法也可以被類實例調用。(classmethod is a non-data descriptor, so it only implements __get__(). This __get__() method completely ignores the instance parameter because, as "class" in the name implies, the method has nothing to do with an instance of the class and only deals with the class itself. What's really nice is the fact that this can still be called from an instance without any issues.)

為什么 __get__() 返回了偏函數 functools.partial 并把 owner 傳入了? 為了理解這個,回憶一下被標記為 classmethod 的方法的參數列表。第一個參數為 class 參數,通常名為 cls,(owner 被賦值給 cls)。這個 class 參數在偏函數方法 partial 中被填充,因此返回的函數可以在被調用時只傳入用戶想要明確提供的參數。真實的實現沒有使用 partial,但是效果相似。(Why does the __get__() method return a functools.partial object with the owner passed in, though. To understand this, think about the parameter list of a function marked as a classmethod. The first parameter is the class parameter, usually named cls. This class parameter is filled in the call to partial so that the returned function can be called with just the arguments the user wants to explicitly provide. The true implementation doesn't use partial, but works similarly.)

同樣的,為了展示主要功能的運作,__name____doc__ 的代碼被忽略。

staticmethod

被標記為 staticmethod 的函數只是依附于類的函數。作為類的一部分,它只是有了特別的命名空間。因為 staticmethod 和 classmethod 都使用描述符實現,所以都可以被派生類繼承。(staticmethod 重要的特性)。(A method marked with staticmethod is strange in that it's a method that is really just a function, but it is "attached" to a class. Being part of the class doesn't do anything other than show users that it is associated with that class and giving it a more specific namespace. Also, interestingly, because staticmethod and classmethod are implemented using descriptors, they're inherited by subclasses.)

staticmethod 的實現比 classmethod 簡單,接受一個函數并當 __get__() 被調用的時候返回它。(The implementation of staticmethod is even simpler than that of classmethod; it just accepts a function and then returns it when __get__() is called.)

class staticmethod:
    def __init__(self, func):
        self.func = func
    
    def __get__(self, instance, owner):
        return self.func
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容