引入描述器
以stackoverflow上關于描述器(descriptor )的疑問開篇,
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進行代理,由這段代碼引出的三點疑問:
- 疑問一:什么是描述器?
- 疑問二:
__get__,__set__,__delete__三種方法的引數 - 疑問三:描述器有哪些應用場景
- 疑問四:property和描述器的區別是什么?
疑問一:什么是描述器?
描述器是一個 實作了 __get__、 __set__和__delete__中1個或多個方法的類物件,當一個類變數指向這樣的一個裝飾器的時候, 訪問這個類變數會呼叫__get__ 方法, 對這個類變數賦值會呼叫__set__ 方法,這種類變數就叫做描述器,
描述器 事實上是一種代理機制:當一個類變數被定義為描述器,對這個類變數的操作,將由此描述器來代理,
疑問二:描述器三種方法的引數
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描述器進行代理,
由以上輸出結果可以得出結論:
引數解釋
__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》
即描述器主要用來接管對實體變數的操作,
實作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類變數:
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')
使用結果:
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的內部機制獲取和設定屬性值,總共有三種方法:
- Getters和Setter,我們可以使用方法來封裝每個實體變數,獲取和設定該實體變數的值,為了確保實體變數不被外部訪問,可以把這些實體變數定義為私有的,所以,訪問物件的屬性需要通過顯式函式:anObject.setPrice(someValue); anObject.getValue(),
- property,我們可以使用內置的property函式將getter,setter(和deleter)函式與屬性名系結,因此,對屬性的參考看起來就像直接訪問那么簡單,但是本質上是呼叫物件的相應函式,例如,anObject.price = someValue; anObject.value,
- 描述器,我們可以將getter,setter(和deleter)函式系結到一個單獨的類中,然后,我們將該類的物件分配給屬性名稱,這時候對每個屬性的參考也像直接訪問一樣,但是本質上是呼叫這個描述器物件相應的方法,例如,anObject.price = someValue; anObject.value,
Getter和Setter這種設計模式不夠Pythonic,雖然在C++和JAVA中很常見,但是Python追求的是簡介,追求的是能夠直接訪問,
附1、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
附2、描述器機制分析資料:
- 官方檔案-descriptor
- understanding-get-and-set-and-python-descriptors
- anyisalin - Python - 描述器
- Python描述器引導(翻譯)
- Properties and Descriptors
記得幫我點贊哦!
精心整理了計算機各個方向的從入門、進階、實戰的視頻課程和電子書,按照目錄合理分類,總能找到你需要的學習資料,還在等什么?快去關注下載吧!!!

念念不忘,必有回響,小伙伴們幫我點個贊吧,非常感謝,
我是職場亮哥,YY高級軟體工程師、四年作業經驗,拒絕咸魚爭當龍頭的斜杠程式員,
聽我說,進步多,程式人生一把梭
如果有幸能幫到你,請幫我點個【贊】,給個關注,如果能順帶評論給個鼓勵,將不勝感激,
職場亮哥文章串列:更多文章

本人所有文章、回答都與著作權保護平臺有合作,著作權歸職場亮哥所有,未經授權,轉載必究!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/158102.html
標籤:其他
