以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
__get__
,__set__
,__delete__
三種方法的參數<!--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)
方法,而且須要返回一個valueinstance.descriptor = value
實際是調用了descriptor.__set__(self, instance, value)
方法,返回值爲None。del instance.descriptor
實際是調用了descriptor.__delete__(self, obj_instance)
方法,返回值爲None咱們想建立一種新形式的實例屬性,除了修改、訪問以外還有一些額外的功能,例如 類型檢查、數值校驗等,就須要用到描述器 《Python Cookbook》
即描述器主要用來接管對實例變量的操做。segmentfault
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
class Staticmethod: def __init__(self, fn): self.fn = fn def __get__(self, instance, cls): return self.fn
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
咱們能夠利用Python的內部機制獲取和設置屬性值。總共有三種方法:
Getter和Setter這種設計模式不夠Pythonic,雖然在C++和JAVA中很常見,可是Python追求的是簡介,追求的是可以直接訪問。
附一、data-descriptor and no-data descriptor
翻譯爲中文其實就是資料描述器和非資料描述器
__get__
和__set__
方法的描述器__get__
方法的描述器二者的區別在於:
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
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
附二、描述器機制分析資料:
記得幫我點贊哦!
精心整理了計算機各個方向的從入門、進階、實戰的視頻課程和電子書,按照目錄合理分類,總能找到你須要的學習資料,還在等什麼?快去關注下載吧!!!
念念不忘,必有迴響,小夥伴們幫我點個贊吧,很是感謝。
我是職場亮哥,YY高級軟件工程師、四年工做經驗,拒絕鹹魚爭當龍頭的斜槓程序員。聽我說,進步多,程序人生一把梭
若是有幸能幫到你,請幫我點個【贊】,給個關注,若是能順帶評論給個鼓勵,將不勝感激。
職場亮哥文章列表:更多文章
本人全部文章、回答都與版權保護平臺有合做,著做權歸職場亮哥全部,未經受權,轉載必究!