您當前的位置:首頁 > 詩詞

深入描述符

作者:由 小明 發表于 詩詞時間:2017-03-27

描述符是一種在多個屬性上重複利用同一個存取邏輯的方式,他能“劫持”那些本對於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

):

print

‘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之美」):

深入描述符

深入描述符

標簽: __  self  value  instance  name