Python描述器

引入描述器

以stackoverflow上關於描述器(descriptor )的疑問開篇。html

class Celsius:

    def __get__(self, instance, owner):
        return 5 * (instance.fahrenheit - 32) / 9

    def __set__(self, instance, value):
        instance.fahrenheit = 32 + 9 * value / 5


class Temperature:

    celsius = Celsius()

    def __init__(self, initial_f):
        self.fahrenheit = initial_f


t = Temperature(212)
print(t.celsius)  # 輸出100.0
t.celsius = 0
print(t.fahrenheit)  # 輸出32.0

以上代碼實現了溫度的攝氏溫度和華氏溫度之間的自動轉換。其中Temperature類含有實例變量fahrenheit和類變量celsius,celsius由描述器Celsius進行代理。由這段代碼引出的三點疑問:python

  1. 疑問一:什麼是描述器?
  2. 疑問二:__get__,__set__,__delete__三種方法的參數
  3. 疑問三:描述器有哪些應用場景
  4. 疑問四:property和描述器的區別是什麼?

<!--more-->linux

疑問一:什麼是描述器?

描述器是一個 實現了 __get____set____delete__中1個或多個方法的類對象。當一個類變量指向這樣的一個裝飾器的時候, 訪問這個類變量會調用__get__ 方法, 對這個類變量賦值會調用__set__ 方法,這種類變量就叫作描述器。git

描述器 事實上是一種代理機制:當一個類變量被定義爲描述器,對這個類變量的操做,將由此描述器來代理。程序員

疑問二:描述器三種方法的參數

class descriptor:
    def __get__(self, instance, owner):
        print(instance)
        print(owner)
        return 'desc'

    def __set__(self, instance, value):
        print(instance)
        print(value)

    def __delete__(self, instance):
        print(instance)

class A:
    a = descriptor()

del A().a  # 輸出<__main__.A object at 0x7f3fc867cbe0>
A().a  # 返回desc,輸出<__main__.A object at 0x7f3fc86741d0>,<class '__main__.A'>
A.a  # 返回desc,輸出None,<class '__main__.A'>
A().a = 5  # 輸出<__main__.A object at 0x7f3fc86744a8>,5
A.a = 5  # 直接修改類A的類變量,也就是a再也不由descriptor描述器進行代理。

由以上輸出結果能夠得出結論:github

參數解釋

  • __get__(self, instance, owner) instance 表示當前實例 owner 表示類自己, 使用類訪問的時候, instance爲None
  • __set__(self, instance, value) instance 表示當前實例, value 右值, 只有實例纔會調用 __set__
  • __delete__(self, instance) instance 表示當前實例

三種方法的本質

  • 訪問:instance.descriptor實際是調用了descriptor.__get__(self, instance, owner)方法,而且須要返回一個value
  • 賦值:instance.descriptor = value實際是調用了descriptor.__set__(self, instance, value)方法,返回值爲None。
  • 刪除:del instance.descriptor實際是調用了descriptor.__delete__(self, obj_instance)方法,返回值爲None

疑問三:描述器有哪些應用場景

咱們想建立一種新形式的實例屬性,除了修改、訪問以外還有一些額外的功能,例如 類型檢查、數值校驗等,就須要用到描述器 《Python Cookbook》

即描述器主要用來接管對實例變量的操做。segmentfault

實現classmethod裝飾器

from functools import partial
from functools import wraps

class Classmethod():
    def __init__(self, fn):
        self.fn = fn

    def __get__(self, instance, owner):
        return wraps(self.fn)(partial(self.fn, owner))

將方法fn的第一個參數固定成實例的類。可參考python官方文檔的另外一種寫法:descriptor設計模式

class ClassMethod(object):
    def __init__(self, fn):
        self.fn = fn

    def __get__(self, instance, owner=None):
        if owner is None:
            owner = type(obj)
        def newfunc(*args):
            return self.f(owner, *args)
        return newfunc

實現staticmethod裝飾器

class Staticmethod:
    def __init__(self, fn):
        self.fn = fn

    def __get__(self, instance, cls):
        return self.fn

實現property裝飾器

class Property:
    def __init__(self, fget, fset=None, fdel=None, doc=''):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        self.doc = doc

    def __get__(self, instance ,owner):
        if instance is not None:
            return self.fget(instance)
        return self

    def __set__(self, instance, value):
        if not callable(self.fset):
            raise AttibuteError('cannot set')
        self.fset(instance, value)

    def __delete__(self, instance):
        if not callable(self.fdel):
            raise AttributeError('cannot delete')
        self.fdel(instance)

    def setter(self, fset):
        self.fset = fset
        return self

    def deleter(self, fdel):
        self.fdel = fdel
        return self

使用自定義的Property來描述farenheit和celsius類變量:app

class Temperature:
    def __init__(self, cTemp):
        self.cTemp = cTemp  # 有一個實例變量cTemp:celsius temperature

    def fget(self):
        return self.celsius * 9 /5 +32

    def fset(self, value):
        self.celsius = (float(value) -32) * 5 /9

    def fdel(self):
        print('Farenhei cannot delete')

    farenheit = Property(fget, fset, fdel, doc='Farenheit temperature')

    def cget(self):
        return self.cTemp

    def cset(self, value):
        self.cTemp = float(value)

    def cdel(self):
        print('Celsius cannot delete')

    celsius = Property(cget, cset, cdel, doc='Celsius temperature')

使用結果:ide

t = Temperature(0)
t.celsius  # 返回0.0
del t.celsius  # 輸出Celsius cannot delete
t.celsius = 5
t.farenheit  # 返回41.0
t.farenheit = 212
t.celsius  # 返回100.0
del t.farenheit  # 輸出Farenhei cannot delete

使用裝飾器的方式來裝飾Temperature的兩個屬性farenheit和celsius:

class Temperature:
    def __init__(self, cTemp):
        self.cTemp = cTemp

    @Property  # celsius = Property(celsius)
    def celsius(self):
        return self.cTemp

    @celsius.setter
    def celsius(self, value):
        self.cTemp = value

    @celsius.deleter
    def celsius(self):
        print('Celsius cannot delete')

    @Property  # farenheit = Property(farenheit)
    def farenheit(self):
        return self.celsius * 9 /5 +32

    @farenheit.setter
    def farenheit(self, value):
        self.celsius = (float(value) -32) * 5 /9

    @farenheit.deleter
    def farenheit(self):
        print('Farenheit cannot delete')

使用結果同直接用描述器描述類變量

實現屬性的類型檢查

首先實現一個類型檢查的描述器Typed

class Typed:
    def __init__(self, name, expected_type):
        # 每一個屬性都有一個名稱和對應的類型
        self.name = name
        self.expected_type = expected_type

    def __get__(self, instance, cls):
        if instance is None:
            return self
        return instance.__dict__[self.name]

    def __set__(self, instance ,value):
        if not isinstance(value, self.expected_type):
            raise TypeError('Attribute {} expected {}'.format(self.name, self.expected_type))
        instance.__dict__[self.name] = value

    def __delete__(self, instance):
        del instance.__dict__[self.name]

而後實現一個Person類,Person類的屬性name和age都由Typed來描述

class Person:
    name = Typed('name', str)
    age = Typed('age', int)

    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age

類型檢查過程:

>>> Person.__dict__
mappingproxy({'__dict__': <attribute '__dict__' of 'Person' objects>,
              '__doc__': None,
              '__init__': <function __main__.Person.__init__>,
              '__module__': '__main__',
              '__weakref__': <attribute '__weakref__' of 'Person' objects>,
              'age': <__main__.Typed at 0x7fe2f440bd68>,
              'name': <__main__.Typed at 0x7fe2f440bc88>})
>>> p = Person('suncle', 18)
>>> p.__dict__
{'age': 18, 'name': 'suncle'}
>>> p = Person(18, 'suncle')
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-88-ca4808b23f89> in <module>()
----> 1 p = Person(18, 'suncle')

<ipython-input-84-f876ec954895> in __init__(self, name, age)
      4 
      5     def __init__(self, name: str, age: int):
----> 6         self.name = name
      7         self.age = age

<ipython-input-83-ac59ba73c709> in __set__(self, instance, value)
     11     def __set__(self, instance ,value):
     12         if not isinstance(value, self.expected_type):
---> 13             raise TypeError('Attribute {} expected {}'.format(self.name, self.expected_type))
     14         instance.__dict__[self.name] = value
     15 

TypeError: Attribute name expected <class 'str'>

可是上述類型檢查的方法存在一些問題,Person類可能有不少屬性,那麼每個屬性都須要使用Typed描述器描述一次。咱們能夠寫一個帶參數的類裝飾器來解決這個問題:

def typeassert(**kwargs):
    def wrap(cls):
        for name, expected_type in kwargs.items():
            setattr(cls, name, Typed(name, expected_type))  # 經典寫法
        return cls
    return wrap

而後使用typeassert類裝飾器從新定義Person類:

@typeassert(name=str, age=int)
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

能夠看到typeassert類裝飾器的參數是傳入的屬性名稱和類型的鍵值對。

若是咱們想讓typeassert類裝飾器自動的識別類的初始化參數類型,而且增長相應的類變量的時候,咱們就能夠藉助inspect庫和python的類型註解實現了:

import inspect
def typeassert(cls):
    params = inspect.signature(cls).parameters
    for name, param in params.items():
        if param.annotation != inspect._empty:
            setattr(cls, name, Typed(name, param.annotation))
    return cls

@typeassert
class Person:
    def __init__(self, name: str, age: int):  # 沒有類型註解的參數不會被託管
        self.name = name
        self.age = age

疑問四:property和描述器的區別

咱們能夠利用Python的內部機制獲取和設置屬性值。總共有三種方法:

  1. Getters和Setter。咱們可使用方法來封裝每一個實例變量,獲取和設置該實例變量的值。爲了確保實例變量不被外部訪問,能夠把這些實例變量定義爲私有的。因此,訪問對象的屬性須要經過顯式函數:anObject.setPrice(someValue); anObject.getValue()。
  2. property。咱們可使用內置的property函數將getter,setter(和deleter)函數與屬性名綁定。所以,對屬性的引用看起來就像直接訪問那麼簡單,可是本質上是調用對象的相應函數。例如,anObject.price = someValue; anObject.value。
  3. 描述器。咱們能夠將getter,setter(和deleter)函數綁定到一個單獨的類中。而後,咱們將該類的對象分配給屬性名稱。這時候對每一個屬性的引用也像直接訪問同樣,可是本質上是調用這個描述器對象相應的方法,例如,anObject.price = someValue; anObject.value。

Getter和Setter這種設計模式不夠Pythonic,雖然在C++和JAVA中很常見,可是Python追求的是簡介,追求的是可以直接訪問。


附一、data-descriptor and no-data descriptor

翻譯爲中文其實就是資料描述器和非資料描述器

  • data-descriptor:同時實現了__get____set__方法的描述器
  • no-data descriptor:只實現了__get__方法的描述器

二者的區別在於:

  • no-data descriptor的優先級低於instance.__dict__
class Int:
    def __get__(self, instance, cls):
        return 3

class A:
    val = Int()

    def __init__(self):
        self.__dict__['val'] = 5

A().val  # 返回5
  • data descriptor的優先級高於instance.__dict__
class Int:
    def __get__(self, instance, cls):
        return 3

    def __set__(self, instance, value):
        pass

class A:
    val = Int()

    def __init__(self):
        self.__dict__['val'] = 5

A().val  # 返回3

附二、描述器機制分析資料:

  1. 官方文檔-descriptor
  2. understanding-get-and-set-and-python-descriptors
  3. anyisalin - Python - 描述器
  4. Python描述器引導(翻譯)
  5. Properties and Descriptors

記得幫我點贊哦!

精心整理了計算機各個方向的從入門、進階、實戰的視頻課程和電子書,按照目錄合理分類,總能找到你須要的學習資料,還在等什麼?快去關注下載吧!!!

resource-introduce

念念不忘,必有迴響,小夥伴們幫我點個贊吧,很是感謝。

我是職場亮哥,YY高級軟件工程師、四年工做經驗,拒絕鹹魚爭當龍頭的斜槓程序員。

聽我說,進步多,程序人生一把梭

若是有幸能幫到你,請幫我點個【贊】,給個關注,若是能順帶評論給個鼓勵,將不勝感激。

職場亮哥文章列表:更多文章

wechat-platform-guide-attention

本人全部文章、回答都與版權保護平臺有合做,著做權歸職場亮哥全部,未經受權,轉載必究!

相關文章
相關標籤/搜索