什么是描述符類?
根據鴨子模型理論,只要具有__get__
方法的類就是描述符類。
如果一個類中具有__get__
和__set__
兩個方法,那么就是數據描述符,。
如果一個類中只有__get__
方法,那么是非數據描述符。
__get__
:當我們用類或者實例來調用該屬性時,Python會返回__get__
函數的結果。
__set__
:當我們用實例來設置屬性值時,Python會調用該函數。對類沒有限制作用。
__delete__
:當我們用實例試圖刪除該屬性時,Python會調用該函數。對類沒有限制作用。
非數據描述類
class Desc:
def __init__(self, value=22):
self.value= value
def __get__(self, ins, cls):
return self.value
class A:
v=Desc()
a=A()
上面的描述符類只有一個__get__
屬性,所以是非數據描述符。
>>> a.v #由於實例中沒有v屬性,所以找到了類的屬性,而類的屬性是一個描述符類實例,所以調用其__get__方法的結果。
22
>>> a.__dict__ #實例的__dict__空空如也。
{}
>>> A.__dict__ #類的__dict__中確實存在v屬性,且是一個Desc object對象。
mappingproxy({'__module__': 'b', '__weakref__': <attribute '__weakref__' of 'A' objects>, '__dict__': <attribute '__dict__' of 'A' objects>, '__doc__': None, 'v': <b.Desc object at 0x7ff010f5f550>})
>>> a.v=30 #我們通過實例設置v屬性,發現成功了。
>>> a.__dict__ #我們發現實例的__dict__中存入了我們剛才設置的屬性
{'v': 30}
>>> A.__dict__ #類的__dict__沒有發生任何變化
mappingproxy({'__module__': 'b', '__weakref__': <attribute '__weakref__' of 'A' objects>, '__dict__': <attribute '__dict__' of 'A' objects>, '__doc__': None, 'v': <b.Desc object at 0x7ff010f5f550>})
>>> a.v #如我們所料,訪問到了a.__dict__中的內容。
30
>>> del a.v #我們刪除實例的屬性v后發現居然還是可以調用a.v,返回的是我們設置之前的值。
>>> a.v
22
>>> A.__dict__ #和前面一樣,沒有發生變化。
mappingproxy({'__module__': 'b', '__weakref__': <attribute '__weakref__' of 'A' objects>, '__dict__': <attribute '__dict__' of 'A' objects>, '__doc__': None, 'v': <b.Desc object at 0x7ff010f5f550>})
通過上面的測試,我們發現非數據描述類有如下特點:
- 如果實例
__dict__
沒有設置同名屬性,那么返回描述類的__get__
方法的結果。 - 如果實例
__dict__
中存在同名屬性,那么返回實例__dict__
中的內容。 - 對我們設置實例的
__dict__
中的行為並不做阻止。所以我說這是查看級別的描述類。
數據描述類
class Desc:
def __init__(self, value=22):
self.value= value
def __get__(self, ins, cls):
return self.value
def __set__(self, ins, value):
self.value=value
#raise AttributeError
class A:
v=Desc()
a=A()
運行結果如下:
>>> a.v
22
>>> a.v=10
>>> a.__dict__ #我們設置a.v后,發現實例的__dict__中仍然空空如也。因為此時調用的是__set__方法,值10存入到了Desc實例的value屬性上了。
{}
>>> A.__dict__
mappingproxy({'__module__': 'b', '__doc__': None, '__dict__': <attribute '__dict__' of 'A' objects>, '__weakref__': <attribute '__weakref__' of 'A' objects>, 'v': <b.Desc object at 0x7f0d2a4de5c0>})
>>> a.v #此時得到的還是Desc的__get__方法返回的結果。
10
>>> del a.v #不允許我們刪除
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: __delete__
>>> A.v=30
>>> A.__dict__
mappingproxy({'__module__': 'b', '__doc__': None, '__dict__': <attribute '__dict__' of 'A' objects>, '__weakref__': <attribute '__weakref__' of 'A' objects>, 'v': 30})
#我們把__set__方法的原來語句注銷,添加raise AttribeError語句,再次運行
>>> a.v=30 #我們在__set__中手動添加了AttributeError異常,所以我們再也不能設置a.v的值了,因此該屬性鞭策了只讀屬性。
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/aaa/proj/b.py", line 8, in __set__
raise AttributeError
AttributeError
>>> A.v=20 #通過類,仍然可以改變屬性
>>> A.__dict__ #改變后,變成了普通屬性20了,這時甚至都已經不再是描述符類了。
mappingproxy({'__doc__': None, '__dict__': <attribute '__dict__' of 'A' objects>, '__module__': 'b', '__weakref__': <attribute '__weakref__' of 'A' objects>, 'v': 20})
>>> del A.v
說明如下:
- 當
__set__
方法存在后,實例設置同名屬性時,完全需要看__set__
的臉色。 - 如果描述類中
__set__
方法存在但是__delete__
方法不存在,那么不能刪除客戶類中的屬性。 - 即使在
__set__
方法中做了限制,這個限制只是對實例而言的,對類沒有起到作用。
把屬性存在描述符類中
class Desc:
def __init__(self, value):
self.value = value
def __get__(self, ins, cls):
return self.value
def __set__(self, ins, value):
self.value = value
def __delete__(self, ins):
raise AttributeError('not allowed to delete attribute name ' )
class A:
name=Desc('JS')
a=A()
執行結果如下:
>>> del a.name
>>> a=A()
>>> b=A()
>>> a.name
'JS'
>>> b.name
'JS'
>>> a.name='CC'
>>> b.name
'CC'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/aaa/proj/b.py", line 10, in __delete__
raise AttributeError('not allowed to delete attribute name ' )
AttributeError: not allowed to delete attribute name
缺點顯而易見,如果有多個實例,那么他們共享一個描述符,所以當一個實例的該屬性發生改變后,其他實例的該屬性也會發生變化。
改善方法:
存入一個字典,把實例的hash作為健存入,這樣可以解決問題。
class Desc:
def __init__(self, value):
self.values={}
def __get__(self, ins, cls):
return self.values[hash(ins)]
def __set__(self, ins, value):
self.values[hash(ins)]=value
def __delete__(self, ins):
raise AttributeError('not allowed to delete attribute name ' )
把數據存入實例中
class Desc:
def __get__(self, ins, cls):
return ins._name
def __set__(self, ins, value):
ins._name=value
def __delete__(self, ins):
raise AttributeError('not allowed to delete attribute name ' )
class A:
name=Desc()
a=A()
執行結果如下:
>>> a=A()
>>> a.name='JS'
>>> a.name
'JS'
>>> a._name='CC'
>>> a.name
'CC'
缺點:我們設置在實例中的變量私密性不太好,可以很容易被改變。
當然,可以做一個私有性的裝飾器,或者利用屬性擴張來解決,這是我在后面會介紹的內容。
補充解釋
__get__(self, ins, cls)
:其中ins為實例對象,在我們上面的例子中是a或者b,cls為a或者b的類,為A
__set__
和__delete__
:ins和上面的含義相同