深入描述符
描述符是一種在多個屬性上重複利用同一個存取邏輯的方式,他能“劫持”那些本對於self。__dict__的操作。描述符通常是一種包含__get__、__set__、__delete__三種方法中至少一種的類,給人的感覺是「把一個類的操作託付與另外一個類」。靜態方法、類方法、property都是構建描述符的類。
我們先看一個簡單的描述符的例子(基於我之前的分享的Python高階程式設計改編,這個PPT建議大家去看看):
class
MyDescriptor
(
object
):
_value
=
‘’
def
__get__
(
self
,
instance
,
klass
):
return
self
。
_value
def
__set__
(
self
,
instance
,
value
):
self
。
_value
=
value
。
swapcase
()
class
Swap
(
object
):
swap
=
MyDescriptor
()
注意MyDescriptor要用新式類。呼叫一下:
In [1]: from descriptor_example import Swap
In [2]: instance = Swap()
In [3]: instance。swap # 沒有報AttributeError錯誤,因為對swap的屬性訪問被描述符類過載了
Out[3]: ‘’
In [4]: instance。swap = ‘make it swap’ # 使用__set__重新設定_value
In [5]: instance。swap
Out[5]: ‘MAKE IT SWAP’
In [6]: instance。__dict__ # 沒有用到__dict__:被劫持了
Out[6]: {}
這就是描述符的威力。我們熟知的staticmethod、classmethod如果你不理解,那麼看一下用Python實現的效果可能會更清楚了:
>>> class myStaticMethod(object):
。。。 def __init__(self, method):
。。。 self。staticmethod = method
。。。 def __get__(self, object, type=None):
。。。 return self。staticmethod
。。。
>>> class myClassMethod(object):
。。。 def __init__(self, method):
。。。 self。classmethod = method
。。。 def __get__(self, object, klass=None):
。。。 if klass is None:
。。。 klass = type(object)
。。。 def newfunc(*args):
。。。 return self。classmethod(klass, *args)
。。。 return newfunc
在實際的生產專案中,描述符有什麼用處呢?首先看MongoEngine中的Field的用法:
from
mongoengine
import
*
class
Metadata
(
EmbeddedDocument
):
tags
=
ListField
(
StringField
())
revisions
=
ListField
(
IntField
())
class
WikiPage
(
Document
):
title
=
StringField
(
required
=
True
)
text
=
StringField
()
metadata
=
EmbeddedDocumentField
(
Metadata
)
有非常多的Field型別,其實它們的基類就是一個描述符,我簡化下,大家看看實現的原理:
class
BaseField
(
object
):
name
=
None
def
__init__
(
self
,
**
kwargs
):
self
。
__dict__
。
update
(
kwargs
)
。。。
def
__get__
(
self
,
instance
,
owner
):
return
instance
。
_data
。
get
(
self
。
name
)
def
__set__
(
self
,
instance
,
value
):
。。。
instance
。
_data
[
self
。
name
]
=
value
很多專案的原始碼看起來很複雜,在抽絲剝繭之後,其實原理非常簡單,複雜的是業務邏輯。
接著我們再看Flask的依賴Werkzeug中的cached_property:
class _Missing(object):
def __repr__(self):
return ‘no value’
def __reduce__(self):
return ‘_missing’
_missing = _Missing()
class cached_property(property):
def __init__(self, func, name=None, doc=None):
self。__name__ = name or func。__name__
self。__module__ = func。__module__
self。__doc__ = doc or func。__doc__
self。func = func
def __set__(self, obj, value):
obj。__dict__[self。__name__] = value
def __get__(self, obj, type=None):
if obj is None:
return self
value = obj。__dict__。get(self。__name__, _missing)
if value is _missing:
value = self。func(obj)
obj。__dict__[self。__name__] = value
return value
其實看類的名字就知道這是快取屬性的,看不懂沒關係,用一下:
class
Foo
(
object
):
@cached_property
def
foo
(
self
):
‘Call me!’
return
42
呼叫下:
In [1]: from cached_property import Foo
。。。: foo = Foo()
。。。:
In [2]: foo。bar
Call me!
Out[2]: 42
In [3]: foo。bar
Out[3]: 42
可以看到在從第二次呼叫bar方法開始,其實用的是快取的結果,並沒有真的去執行。
說了這麼多描述符的用法。我們寫一個做欄位驗證的描述符:
class
Quantity
(
object
):
def
__init__
(
self
,
name
):
self
。
name
=
name
def
__set__
(
self
,
instance
,
value
):
if
value
>
0
:
instance
。
__dict__
[
self
。
name
]
=
value
else
:
raise
ValueError
(
‘value must be > 0’
)
class
Rectangle
(
object
):
height
=
Quantity
(
‘height’
)
width
=
Quantity
(
‘width’
)
def
__init__
(
self
,
height
,
width
):
self
。
height
=
height
self
。
width
=
width
@property
def
area
(
self
):
return
self
。
height
*
self
。
width
我們試一試:
In
[
1
]:
from
rectangle
import
Rectangle
In
[
2
]:
r
=
Rectangle
(
10
,
20
)
In
[
3
]:
r
。
area
Out
[
3
]:
200
In
[
4
]:
r
=
Rectangle
(
-
1
,
20
)
——————————————————————————————————————-
ValueError
Traceback
(
most
recent
call
last
)
<
ipython
-
input
-
5
-
5
a7fc56e8a
>
in
<
module
>
()
——>
1
r
=
Rectangle
(
-
1
,
20
)
/
Users
/
dongweiming
/
mp
/
2017
-
03
-
23
/
rectangle
。
py
in
__init__
(
self
,
height
,
width
)
15
16
def
__init__
(
self
,
height
,
width
):
——->
17
self
。
height
=
height
18
self
。
width
=
width
19
/
Users
/
dongweiming
/
mp
/
2017
-
03
-
23
/
rectangle
。
py
in
__set__
(
self
,
instance
,
value
)
7
instance
。
__dict__
[
self
。
name
]
=
value
8
else
:
——>
9
raise
ValueError
(
‘value must be > 0’
)
10
11
ValueError
:
value
must
be
>
0
看到了吧,我們在描述符的類裡面對傳值進行了驗證。ORM就是這麼玩的!
但是上面的這個實現有個缺點,就是不太自動化,你看 height =Quantity(‘height’),這得讓屬性和Quantity的name都叫做height,那麼可不可以不用指定name呢?當然可以,不過實現的要複雜很多:
class
Quantity
(
object
):
__counter
=
0
def
__init__
(
self
):
cls
=
self
。
__class__
prefix
=
cls
。
__name__
index
=
cls
。
__counter
self
。
name
=
‘_{}#{}’
。
format
(
prefix
,
index
)
cls
。
__counter
+=
1
def
__get__
(
self
,
instance
,
owner
):
if
instance
is
None
:
return
self
return
getattr
(
instance
,
self
。
name
)
。。。
class
Rectangle
(
object
):
height
=
Quantity
()
width
=
Quantity
()
。。。
Quantity的name相當於類名+計時器,這個計時器每呼叫一次就疊加1,用此區分。有一點值得提一提,在__get__中的:
if
instance
is
None
:
return
self
在很多地方可見,比如之前提到的MongoEngine中的BaseField。這是由於直接呼叫Rectangle。height這樣的屬性時候會報AttributeError, 因為描述符是例項上的屬性。
PS:這個靈感來自《Fluent Python》,書中還有一個我認為設計非常好的例子。就是當要驗證的內容種類很多的時候,如何更好地擴充套件的問題。現在假設我們除了驗證傳入的值要大於0,還得驗證不能為空和必須是數字(當然三種驗證在一個方法中驗證也是可以接受的,我這裡就是個演示),我們先寫一個abc的基類:
class
Validated
(
abc
。
ABC
):
__counter
=
0
def
__init__
(
self
):
cls
=
self
。
__class__
prefix
=
cls
。
__name__
index
=
cls
。
__counter
self
。
name
=
‘_{}#{}’
。
format
(
prefix
,
index
)
cls
。
__counter
+=
1
def
__get__
(
self
,
instance
,
owner
):
if
instance
is
None
:
return
self
else
:
return
getattr
(
instance
,
self
。
name
)
def
__set__
(
self
,
instance
,
value
):
value
=
self
。
validate
(
instance
,
value
)
setattr
(
instance
,
self
。
name
,
value
)
@abc。abstractmethod
def
validate
(
self
,
instance
,
value
):
“”“return validated value or raise ValueError”“”
現在新加一個檢查型別,新增一個繼承了Validated的、包含檢查的validate方法的類就可以了:
class
Quantity
(
Validated
):
def
validate
(
self
,
instance
,
value
):
if
value
<=
0
:
raise
ValueError
(
‘value must be > 0’
)
return
value
class
NonBlank
(
Validated
):
def
validate
(
self
,
instance
,
value
):
value
=
value
。
strip
()
if
len
(
value
)
==
0
:
raise
ValueError
(
‘value cannot be empty or blank’
)
return
value
前面展示的描述符都是一個類,那麼可不可以用函式來實現呢?也是可以的:
def
quantity
():
try
:
quantity
。
counter
+=
1
except
AttributeError
:
quantity
。
counter
=
0
storage_name
=
‘_{}:{}’
。
format
(
‘quantity’
,
quantity
。
counter
)
def
qty_getter
(
instance
):
return
getattr
(
instance
,
storage_name
)
def
qty_setter
(
instance
,
value
):
if
value
>
0
:
setattr
(
instance
,
storage_name
,
value
)
else
:
raise
ValueError
(
‘value must be > 0’
)
return
property
(
qty_getter
,
qty_setter
)
這些都掌握了,你也就算熟悉描述符啦,加油?!
PS:本文全部程式碼可以在微信公眾號文章程式碼庫專案(dongweiming/mp)中找到。
無恥的廣告:《Python Web開發實戰》上市了!
歡迎關注本人的微信公眾號獲取更多Python相關的內容(也可以直接搜尋「Python之美」):