在寫上一篇文章的時候遇到了描述符,本來以為很簡單,看了一些別人寫的博客,結(jié)果發(fā)現(xiàn)遠不如我想的那么簡單,一大堆概念向我砸過來,一時間難以接受,不甚理解,需要反反復復的斟酌,才能大致明白其用意與用法。所以決定把面向?qū)ο竺枋龇糠謫为毮贸鰜韺懸黄恼?,但愿寫出來之后,過幾天我自己還能看的明白。
官方說法:python描述符是一個“綁定行為”的對象屬性,在描述符協(xié)議中,它可以通過方法重寫屬性的訪問。這些方法有 __get__()
, __set__()
, 和__delete__()
。如果這些方法中的任何一個被定義在一個對象中,那么這個對象就是一個描述符。
說啥呢,描述符是一個對象???我覺著吧,描述符應該是一個類,由這個類實例化的對象成為描述符對象
這么說來,描述符本質(zhì)就是一個新式類,在這個新式類中,至少實現(xiàn)了__get__()
,__set__()
,__delete__()
中的一個,這三者也被稱為描述符協(xié)議。
__get__()
和__set__()
__set__()
這兩者的區(qū)別是在訪問屬性時的搜索順序上:
搜索鏈(或者優(yōu)先鏈)的順序:數(shù)據(jù)描述符>實體屬性(存儲在實體的dict中)>非數(shù)據(jù)描述符。解釋如下:
獲取一個屬性的時候:
首先,看這個屬性是不是一個數(shù)據(jù)描述符,如果是,就直接執(zhí)行描述符的__get__()
,并返回值。
其次,如果這個屬性不是數(shù)據(jù)描述符,那就按常規(guī)去從__dict__
里面取。如果__dict__
里面還沒有,但這是一個非數(shù)據(jù)描述符,則執(zhí)行非數(shù)據(jù)描述符的__get__()
方法,并返回。
最后,找不到的屬性觸發(fā)__getattr__()
執(zhí)行
而設(shè)置一個屬性的值時,訪問的順序又有所不同,請看以下講解。
三個方法(協(xié)議):
__get__(self, instance, owner)
:調(diào)用一個屬性時,觸發(fā)
__set__(self, instance, value)
:為一個屬性賦值時,觸發(fā)
__delete__(self, instance)
:采用del刪除屬性時,觸發(fā)
其中,instance是這個描述符屬性所在的類的實體,而owner是描述符所在的類。
那么以上的 self, instance owner 到底指的是個什么東西呢?我們先來看一個描述符定義:
class Desc(object): def __get__(self, instance, owner): print('__get__...') print('self : \t\t', self) print('instance : \t', instance) print('owner : \t', owner) print('='*40, '\n') def __set__(self, instance, value): print('__set__...') print('self : \t\t', self) print('instance : \t', instance) print('value : \t', value) print('='*40, '\n')class TestDesc(object): x = Desc()#以下為測試代碼t = TestDesc()t.x#以下為輸出信息:__get__...self : <__main__.Desc object at 0x0000000002B0B828>instance : <__main__.TestDesc object at 0x0000000002B0BA20>owner : <class '__main__.TestDesc'>========================================
可以看到,實例化類TestDesc后,調(diào)用對象t訪問其屬性x,會自動調(diào)用類Desc的__get__
方法,由輸出信息可以看出:
?、?self: Desc的實例對象,其實就是TestDesc的屬性x
?、?instance: TestDesc的實例對象,其實就是t
?、?owner: 即誰擁有這些東西,當然是 TestDesc這個類,它是最高統(tǒng)治者,其他的一些都是包含在它的內(nèi)部或者由它生出來的
包含這三個方法的新式類稱為描述符,由這個類產(chǎn)生的實例進行屬性的調(diào)用/賦值/刪除,并不會觸發(fā)這三個方法,那何時,何地,會觸發(fā)這三個方法的執(zhí)行呢?
__delete__()
方法,并raise 異常。這些都是摘抄的,我確實不知道描述符有什么用,也不知道為什么要用描述符,先記下吧。
一 描述符本身應該定義成新式類,owner類也應該是新式類
二 必須把描述符定義成這個類的類屬性,不能為定義到構(gòu)造函數(shù)中
三 要嚴格遵循該優(yōu)先級,優(yōu)先級由高到底分別是
1.類屬性
2.數(shù)據(jù)描述符
3.實例屬性
4.非數(shù)據(jù)描述符
5.找不到的屬性觸發(fā)__getattr__()
class Str: def __get__(self, instance, owner): print('Str調(diào)用') def __set__(self, instance, value): print('Str設(shè)置...') def __delete__(self, instance): print('Str刪除...')class People: name=Str() def __init__(self,name,age): #name被Str類代理 self.name=name self.age=age#基于上面的演示,我們已經(jīng)知道,在一個類中定義描述符它就是一個類屬性,存在于類的屬性字典中,而不是實例的屬性字典#那既然描述符被定義成了一個類屬性,直接通過類名也一定可以調(diào)用吧,沒錯People.name #恩,調(diào)用類屬性name,本質(zhì)就是在調(diào)用描述符Str,觸發(fā)了__get__(),類去操作屬性時,會把None傳給instancePeople.name='egon' #那賦值呢,我去,并沒有觸發(fā)__set__()del People.name #趕緊試試del,我去,也沒有觸發(fā)__delete__()'''原因:描述符在使用時被定義成另外一個類的類屬性,因而類屬性比二次加工的描述符偽裝而來的類屬性有更高的優(yōu)先級People.name #恩,調(diào)用類屬性name,找不到就去找描述符偽裝的類屬性name,觸發(fā)了__get__()People.name='egon'#那賦值呢,直接賦值了一個類屬性,它擁有更高的優(yōu)先級,相當于覆蓋了描述符,肯定不會觸發(fā)描述符的__set__()del People.name #同上'''
要驗證,需要將實例的屬性名與類的數(shù)據(jù)描述符同名
class Str: def __get__(self, instance, owner): print('Str調(diào)用') def __set__(self, instance, value): print('Str設(shè)置...') def __delete__(self, instance): print('Str刪除...')class People: name=Str() def __init__(self,name,age): self.name=name self.age=agep1=People('egon',18)#如果描述符是一個數(shù)據(jù)描述符(即有__get__又有__set__),那么p1.name的調(diào)用與賦值都是觸發(fā)描述符的操作,#與p1本身無關(guān)了,相當于覆蓋了實例的屬性p1.name='egonnnnnn'p1.nameprint(p1.__dict__)#實例屬性字典中沒有name,因為name是數(shù)據(jù)描述符,優(yōu)先級高于實例屬性,查看/賦值/刪除都是跟描述符有關(guān),與實例無關(guān)del p1.name
class Foo: def func(self): print('我胡漢三又回來了')f1=Foo()f1.func() #調(diào)用類的方法,也可以說是調(diào)用非數(shù)據(jù)描述符#函數(shù)是一個非數(shù)據(jù)描述符對象(一切皆對象么)print(dir(Foo.func))print(hasattr(Foo.func,'__set__'))print(hasattr(Foo.func,'__get__'))print(hasattr(Foo.func,'__delete__'))#有人可能會問,描述符不都是類么,函數(shù)怎么算也應該是一個對象啊,怎么就是描述符了#笨蛋哥,描述符是類沒問題,描述符在應用的時候不都是實例化成一個類屬性么#函數(shù)就是一個由非描述符類實例化得到的對象#沒錯,字符串也一樣f1.func='這是實例屬性啊'print(f1.func)del f1.func #刪掉了非數(shù)據(jù)f1.func()
class Foo: def __get__(self, instance, owner): print('get')class Room: name=Foo() def __init__(self,name,width,length): self.name=name self.width=width self.length=length#name是一個非數(shù)據(jù)描述符,因為name=Foo()而Foo沒有實現(xiàn)set方法,因而比實例屬性有更低的優(yōu)先級#對實例的屬性操作,觸發(fā)的都是實例自己的r1=Room('廁所',1,1)r1.namer1.name='廚房'
python是弱類型語言,即參數(shù)的賦值沒有類型限制,下面我們通過描述符機制來實現(xiàn)類型限制功能
class Typed: def __init__(self,name,expected_type): self.name=name self.expected_type=expected_type def __get__(self, instance, owner): print('get--->',instance,owner) if instance is None: return self return instance.__dict__[self.name] def __set__(self, instance, value): print('set--->',instance,value) if not isinstance(value,self.expected_type): raise TypeError('Expected %s' %str(self.expected_type)) instance.__dict__[self.name]=value def __delete__(self, instance): print('delete--->',instance) instance.__dict__.pop(self.name)def typeassert(**kwargs): def decorate(cls): print('類的裝飾器開始運行啦------>',kwargs) for name,expected_type in kwargs.items(): setattr(cls,name,Typed(name,expected_type)) return cls return decorate#有參:1.運行typeassert(...)返回結(jié)果是decorate,此時參數(shù)都傳給kwargs 2.People=decorate(People)@typeassert(name=str,age=int,salary=float) class People: def __init__(self,name,age,salary): self.name=name self.age=age self.salary=salaryprint(People.__dict__)p1=People('egon',18,3333.3)
為了讓描述符能夠正常工作,它們必須定義在類的層次上。如果你不這么做,那么Python無法自動為你調(diào)用__get__
和__set__
方法。
訪問類層次上的描述符可以自動調(diào)用__get__
。但是訪問實例層次上的描述符只會返回描述符本身,真是魔法一般的存在啊。
確保實例的數(shù)據(jù)只屬于實例本身,否則所有的實例都共享相同的數(shù)據(jù)
class Desc(object): def __init__(self, name): self.name = name def __get__(self, instance, owner): print('__get__...') print('name = ', self.name) print('=' * 40, '\n')class TestDesc(object): x = Desc('x') def __init__(self): self.y = Desc('y')# 以下為測試代碼t = TestDesc()t.xprint(t.__dict__)print(t.y)'''輸出結(jié)果:__get__...name = x======================================== {'y': <__main__.Desc object at 0x7f514d6ba9e8>}<__main__.Desc object at 0x7f514d6ba9e8>'''
如果把問題換成——一個對象要滿足什么條件,它才是描述符呢——那是不是回答就非常簡單了?
答:只要定義了(set,get,delete)方法中的任意一種或幾種,它就是個描述符。
那么,繼續(xù)問:怎么判斷一個對象定義了這三種方法呢?
立馬有人可能就會回答:你是不是傻啊?看一下不就看出來了。。。
問題是,你看不到的時候呢?python內(nèi)置的staticmethod,classmethod怎么看?
正確的回答應該是:看這個對象的dict。
寫到這里,我得先停一下,來解釋一個問題。不知道讀者有沒有發(fā)現(xiàn),上面我一直再說“對象”,“對象”,而實際上指的明明是一個類???在python中,這樣稱呼又是妥當?shù)摹R驗?,“一切皆對象”,類,不過也是元類的一種對象而已。
要看對象的dict好辦,直接dir(對象)就行了?,F(xiàn)在可以寫出檢測對象是不是描述符的方法了:
def has_descriptor_attrs(obj): return set(['__get__', '__set__', '__delete__']).intersection(dir(obj))def is_descriptor(obj): '''obj can be instance of descriptor or the descriptor class''' return bool(has_descriptor_attrs(obj))def has_data_descriptor_attrs(obj): return set(['__set__', '__delete__']) & set(dir(obj))def is_data_descriptor(obj): return bool(has_data_descriptor_attrs(obj))print(is_descriptor(classmethod), is_data_descriptor(classmethod))print(is_descriptor(staticmethod), is_data_descriptor(staticmethod))print(is_data_descriptor(property))'''輸出:(True, False)(True, False)True看來,特性(property)是數(shù)據(jù)描述符'''
class Foo: @property def attr(self): print('getting attr') return 'attr value' def bar(self): passfoo = Foo()
上面這個例子中, attr 是類 Foo 的一個成員函數(shù),可通過語句 foo.attr() 被調(diào)用。 但當它被 @property 修飾后,這個成員函數(shù)將不再是一個函數(shù),而變?yōu)橐粋€描述符。 bar 是一個未被修飾的成員函數(shù)。 type(Foo.attr) 與 type(Foo.bar) 的結(jié)果分別為:
class Foo: @property def AAA(self): print('get的時候運行我啊') @AAA.setter def AAA(self,value): print('set的時候運行我啊') @AAA.deleter def AAA(self): print('delete的時候運行我啊')#只有在屬性AAA定義property后才能定義AAA.setter,AAA.deleterf1=Foo()f1.AAAf1.AAA='aaa'del f1.AAA#方式2class Foo: def get_AAA(self): print('get的時候運行我啊') def set_AAA(self,value): print('set的時候運行我啊') def delete_AAA(self): print('delete的時候運行我啊') AAA=property(get_AAA,set_AAA,delete_AAA) #內(nèi)置property三個參數(shù)與get,set,delete一一對應f1=Foo()f1.AAAf1.AAA='aaa'del f1.AAA
property將一個函數(shù)變成了類似于屬性的使用,無非只是省略了一個括號而已,可是這有什么意義?以屬性的方式來調(diào)用函數(shù),換句話說,我以為我調(diào)用的是屬性,但是其實是函數(shù),這樣就完成了一個封裝,不需要setter和getter,而直接將setter和getter內(nèi)嵌進去,大大減少了代碼量,使代碼簡潔美觀
案例一:
class Goods: def __init__(self): # 原價 self.original_price = 100 # 折扣 self.discount = 0.8 @property def price(self): # 實際價格 = 原價 * 折扣 new_price = self.original_price * self.discount return new_price @price.setter def price(self, value): self.original_price = value @price.deleter def price(self): del self.original_priceobj = Goods()obj.price # 獲取商品價格obj.price = 200 # 修改商品原價print(obj.price)del obj.price # 刪除商品原價
案例二:
#實現(xiàn)類型檢測功能#第一關(guān):class People: def __init__(self,name): self.name=name @property def name(self): return self.name# p1=People('alex') #property自動實現(xiàn)了set和get方法屬于數(shù)據(jù)描述符,比實例屬性優(yōu)先級高,#所以你這面寫會觸發(fā)property內(nèi)置的set,拋出異常#第二關(guān):修訂版class People: def __init__(self,name): self.name=name #實例化就觸發(fā)property @property def name(self): # return self.name #無限遞歸 print('get------>') return self.DouNiWan @name.setter def name(self,value): print('set------>') self.DouNiWan=value @name.deleter def name(self): print('delete------>') del self.DouNiWanp1=People('alex') #self.name實際是存放到self.DouNiWan里print(p1.name)print(p1.name)print(p1.name)print(p1.__dict__)p1.name='egon'print(p1.__dict__)del p1.nameprint(p1.__dict__)#第三關(guān):加上類型檢查class People: def __init__(self,name): self.name=name #實例化就觸發(fā)property @property def name(self): # return self.name #無限遞歸 print('get------>') return self.DouNiWan @name.setter def name(self,value): print('set------>') if not isinstance(value,str): raise TypeError('必須是字符串類型') self.DouNiWan=value @name.deleter def name(self): print('delete------>') del self.DouNiWanp1=People('alex') #self.name實際是存放到self.DouNiWan里p1.name=1
以上就是關(guān)于描述符的記錄,邊寫邊思考,貌似對描述符理解到了一定的程度,至少我能說明白他是怎么回事了,只是自己還沒有在實際中用過,也不知道應該在哪些場景下去使用,毋庸置疑,我會忘記它的,待到忘記的時候再來看吧。
好了,下一篇就是元類和苦大仇深的ORM了。