描述符和property(有意思可是很差懂) Python從入門到放棄

一句話歸納:描述符就是可重用的屬性

在這裏我要告訴你:從根本上講,描述符就是能夠重複使用的屬性。也就是說,描述符可讓你編寫這樣的代碼:python

Python程序員

1安全

2app

3函數

4oop

f = Foo()this

b = f.bar編碼

f.bar = cspa

del f.bar.net

而在解釋器執行上述代碼時,當發現你試圖訪問屬性(b = f.bar)、對屬性賦值(f.bar = c)或者刪除一個實例變量的屬性(del f.bar)時,就會去調用自定義的方法。

讓咱們先來解釋一下爲何把對函數的調用假裝成對屬性的訪問是大有好處的。

property——把函數調用假裝成對屬性的訪問

想象一下你正在編寫管理電影信息的代碼。你最後寫好的Movie類可能看上去是這樣的:

Python

1

2

3

4

5

6

7

8

9

10

class Movie(object):

    def __init__(self, title, rating, runtime, budget, gross):

        self.title = title

        self.rating = rating

        self.runtime = runtime

        self.budget = budget

        self.gross = gross

 

    def profit(self):

        return self.gross - self.budget

你開始在項目的其餘地方使用這個類,可是以後你意識到:若是不當心給電影打了負分怎麼辦?你以爲這是錯誤的行爲,但願Movie類能夠阻止這個錯誤。 你首先想到的辦法是將Movie類修改成這樣:

Python

1

2

3

4

5

6

7

8

9

10

11

12

class Movie(object):

    def __init__(self, title, rating, runtime, budget, gross):

        self.title = title

        self.rating = rating

        self.runtime = runtime

        self.gross = gross

        if budget < 0:

            raise ValueError("Negative value not allowed: %s" % budget)

        self.budget = budget

 

    def profit(self):

        return self.gross - self.budget

但這行不通。由於其餘部分的代碼都是直接經過Movie.budget來賦值的——這個新修改的類只會在__init__方法中捕獲錯誤的數據,但對於已經存在的類實例就無能爲力了。若是有人試着運行m.budget = -100,那麼誰也無法阻止。做爲一個Python程序員同時也是電影迷,你該怎麼辦?

幸運的是,Python的property解決了這個問題。若是你從未見過property的用法,下面是一個示例:

Python

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

class Movie(object):

    def __init__(self, title, rating, runtime, budget, gross):

        self._budget = None

 

        self.title = title

        self.rating = rating

        self.runtime = runtime

        self.gross = gross

        self.budget = budget

 

    @property

    def budget(self):

        return self._budget

 

    @budget.setter

    def budget(self, value):

        if value < 0:

            raise ValueError("Negative value not allowed: %s" % value)

        self._budget = value

 

    def profit(self):

        return self.gross - self.budget

 

m = Movie('Casablanca', 97, 102, 964000, 1300000)

print m.budget       # calls m.budget(), returns result

try:

    m.budget = -100  # calls budget.setter(-100), and raises ValueError

except ValueError:

    print "Woops. Not allowed"

 

964000

Woops. Not allowed

咱們用@property裝飾器指定了一個getter方法,用@budget.setter裝飾器指定了一個setter方法。當咱們這麼作時,每當有人試着訪問budget屬性,Python就會自動調用相應的getter/setter方法。比方說,當遇到m.budget = value這樣的代碼時就會自動調用budget.setter。

花點時間來欣賞一下Python這麼作是多麼的優雅:若是沒有property,咱們將不得不把全部的實例屬性隱藏起來,提供大量顯式的相似get_budget和set_budget方法。像這樣編寫類的話,使用起來就會不斷的去調用這些getter/setter方法,這看起來就像臃腫的Java代碼同樣。更糟的是,若是咱們不採用這種編碼風格,直接對實例屬性進行訪問。那麼稍後就無法以清晰的方式增長對非負數的條件檢查——咱們不得不從新建立set_budget方法,而後搜索整個工程中的源代碼,將m.budget = value這樣的代碼替換爲m.set_budget(value)。太蛋疼了!!

所以,property讓咱們將自定義的代碼同變量的訪問/設定聯繫在了一塊兒,同時爲你的類保持一個簡單的訪問屬性的接口。幹得漂亮!

property的不足

對property來講,最大的缺點就是它們不能重複使用。舉個例子,假設你想爲rating,runtime和gross這些字段也添加非負檢查。下面是修改過的新類:

Python

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

class Movie(object):

    def __init__(self, title, rating, runtime, budget, gross):

        self._rating = None

        self._runtime = None

        self._budget = None

        self._gross = None

 

        self.title = title

        self.rating = rating

        self.runtime = runtime

        self.gross = gross

        self.budget = budget

 

    #nice

    @property

    def budget(self):

        return self._budget

 

    @budget.setter

    def budget(self, value):

        if value < 0:

            raise ValueError("Negative value not allowed: %s" % value)

        self._budget = value

 

    #ok    

    @property

    def rating(self):

        return self._rating

 

    @rating.setter

    def rating(self, value):

        if value < 0:

            raise ValueError("Negative value not allowed: %s" % value)

        self._rating = value

 

    #uhh...

    @property

    def runtime(self):

        return self._runtime

 

    @runtime.setter

    def runtime(self, value):

        if value < 0:

            raise ValueError("Negative value not allowed: %s" % value)

        self._runtime = value        

 

    #is this forever?

    @property

    def gross(self):

        return self._gross

 

    @gross.setter

    def gross(self, value):

        if value < 0:

            raise ValueError("Negative value not allowed: %s" % value)

        self._gross = value        

 

    def profit(self):

        return self.gross - self.budget

能夠看到代碼增長了很多,但重複的邏輯也出現了很多。雖然property可讓類從外部看起來接口整潔漂亮,可是卻作不到內部一樣整潔漂亮。

描述符登場(最終的大殺器)

這就是描述符所解決的問題。描述符是property的升級版,容許你爲重複的property邏輯編寫單獨的類來處理。下面的示例展現了描述符是如何工做的(如今還沒必要擔憂NonNegative類的實現):

Python

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

from weakref import WeakKeyDictionary

 

class NonNegative(object):

    """A descriptor that forbids negative values"""

    def __init__(self, default):

        self.default = default

        self.data = WeakKeyDictionary()

 

    def __get__(self, instance, owner):

        # we get here when someone calls x.d, and d is a NonNegative instance

        # instance = x

        # owner = type(x)

        return self.data.get(instance, self.default)

 

    def __set__(self, instance, value):

        # we get here when someone calls x.d = val, and d is a NonNegative instance

        # instance = x

        # value = val

        if value < 0:

            raise ValueError("Negative value not allowed: %s" % value)

        self.data[instance] = value

 

class Movie(object):

 

    #always put descriptors at the class-level

    rating = NonNegative(0)

    runtime = NonNegative(0)

    budget = NonNegative(0)

    gross = NonNegative(0)

 

    def __init__(self, title, rating, runtime, budget, gross):

        self.title = title

        self.rating = rating

        self.runtime = runtime

        self.budget = budget

        self.gross = gross

 

    def profit(self):

        return self.gross - self.budget

 

m = Movie('Casablanca', 97, 102, 964000, 1300000)

print m.budget  # calls Movie.budget.__get__(m, Movie)

m.rating = 100  # calls Movie.budget.__set__(m, 100)

try:

    m.rating = -1   # calls Movie.budget.__set__(m, -100)

except ValueError:

    print "Woops, negative value"

 

964000

Woops, negative value

這裏引入了一些新的語法,咱們一條條的來看:

NonNegative是一個描述符對象,由於它定義了__get__,__set__或__delete__方法。

Movie類如今看起來很是清晰。咱們在類的層面上建立了4個描述符,把它們當作普通的實例屬性。顯然,描述符在這裏爲咱們作非負檢查。

訪問描述符

當解釋器遇到print m.buget時,它就會把budget看成一個帶有__get__ 方法的描述符,調用Movie.budget.__get__方法並將方法的返回值打印出來,而不是直接傳遞m.budget來打印。這和你訪問一個property類似,Python自動調用一個方法,同時返回結果。

__get__接收2個參數:一個是點號左邊的實例對象(在這裏,就是m.budget中的m),另外一個是這個實例的類型(Movie)。在一些Python文檔中,Movie被稱做描述符的全部者(owner)。若是咱們須要訪問Movie.budget,Python將會調用Movie.budget.__get__(None, Movie)。能夠看到,第一個參數要麼是全部者的實例,要麼是None。這些輸入參數可能看起來很怪,可是這裏它們告訴了你描述符屬於哪一個對象的一部分。當咱們看到NonNegative類的實現時這一切就合情合理了。

對描述符賦值

當解釋器看到m.rating = 100時,Python識別出rating是一個帶有__set__方法的描述符,因而就調用Movie.rating.__set__(m, 100)。和__get__同樣,__set__的第一個參數是點號左邊的類實例(m.rating = 100中的m)。第二個參數是所賦的值(100)。

刪除描述符

爲了說明的完整,這裏提一下刪除。若是你調用del m.budget,Python就會調用Movie.budget.__delete__(m)。

NonNegative類是如何工做的?

帶着前面的困惑,咱們終於要揭示NonNegative類是如何工做的了。每一個NonNegative的實例都維護着一個字典,其中保存着全部者實例和對應數據的映射關係。當咱們調用m.budget時,__get__方法會查找與m相關聯的數據,並返回這個結果(若是這個值不存在,則會返回一個默認值)。__set__採用的方式相同,可是這裏會包含額外的非負檢查。咱們使用WeakKeyDictionary來取代普通的字典以防止內存泄露——咱們可不想僅僅由於它在描述符的字典中就讓一個無用
的實例一直存活着。

使用描述符會有一點彆扭。由於它們做用於類的層次上,每個類實例都共享同一個描述符。這就意味着對不一樣的實例對象而言,描述符不得不手動地管理
不一樣的狀態,同時須要顯式的將類實例做爲第一個參數準確傳遞給__get__、__set__以及__delete__方法。

我但願這個例子解釋清楚了描述符能夠用來作什麼——它們提供了一種方法將property的邏輯隔離到單獨的類中來處理。若是你發現本身正在不一樣的property之間重複着相同的邏輯,那麼本文也許會成爲一個線索供你思考爲什麼用描述符重構代碼是值得一試的。

祕訣和陷阱

把描述符放在類的層次上(class level)

爲了讓描述符可以正常工做,它們必須定義在類的層次上。若是你不這麼作,那麼Python沒法自動爲你調用__get__和__set__方法。

Python

1

2

3

4

5

6

7

8

9

class Broken(object):

    y = NonNegative(5)

    def __init__(self):

        self.x = NonNegative(0)  # NOT a good descriptor

 

b = Broken()

print "X is %s, Y is %s" % (b.x, b.y)

 

X is <__main__.NonNegative object at 0x10432c250>, Y is 5

能夠看到,訪問類層次上的描述符y能夠自動調用__get__。可是訪問實例層次上的描述符x只會返回描述符自己,真是魔法通常的存在啊。

確保實例的數據只屬於實例自己 

你可能會像這樣編寫NonNegative描述符:

Python

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

class BrokenNonNegative(object):

    def __init__(self, default):

        self.value = default

 

    def __get__(self, instance, owner):

        return self.value

 

    def __set__(self, instance, value):

        if value < 0:

            raise ValueError("Negative value not allowed: %s" % value)

        self.value = value

 

class Foo(object):

    bar = BrokenNonNegative(5)

 

f = Foo()

try:

    f.bar = -1

except ValueError:

    print "Caught the invalid assignment"

 

Caught the invalid assignment

這麼作看起來彷佛能正常工做。但這裏的問題就在於全部Foo的實例都共享相同的bar,這會產生一些使人痛苦的結果:

Python

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

class Foo(object):

    bar = BrokenNonNegative(5)

 

f = Foo()

g = Foo()

 

print "f.bar is %s\ng.bar is %s" % (f.bar, g.bar)

print "Setting f.bar to 10"

f.bar = 10

print "f.bar is %s\ng.bar is %s" % (f.bar, g.bar)  #ouch

f.bar is 5

g.bar is 5

Setting f.bar to 10

f.bar is 10

g.bar is 10

這就是爲何咱們要在NonNegative中使用數據字典的緣由。__get__和__set__的第一個參數告訴咱們須要關心哪個實例。NonNegative使用這個參數做爲字典的key,爲每個Foo實例單獨保存一份數據。

Python

1

2

3

4

5

6

7

8

9

10

11

12

13

14

class Foo(object):

    bar = NonNegative(5)

 

f = Foo()

g = Foo()

print "f.bar is %s\ng.bar is %s" % (f.bar, g.bar)

print "Setting f.bar to 10"

f.bar = 10

print "f.bar is %s\ng.bar is %s" % (f.bar, g.bar)  #better

f.bar is 5

g.bar is 5

Setting f.bar to 10

f.bar is 10

g.bar is 5

這就是描述符最使人感到彆扭的地方(坦白的說,我不理解爲何Python不讓你在實例的層次上定義描述符,而且老是須要將實際的處理分發給__get__和__set__。這麼作行不通必定是有緣由的)

注意不可哈希的描述符全部者

NonNegative類使用了一個字典來單獨保存專屬於實例的數據。這個通常來講是沒問題的,除非你用到了不可哈希(unhashable)的對象:

Python

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

class MoProblems(list):  #you can't use lists as dictionary keys

    x = NonNegative(5)

 

m = MoProblems()

print m.x  # womp womp

 

---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-8-dd73b177bd8d> in <module>()

      3

      4 m = MoProblems()

----> 5 print m.x  # womp womp

 

<ipython-input-3-6671804ce5d5> in __get__(self, instance, owner)

      9         # instance = x

     10         # owner = type(x)

---> 11         return self.data.get(instance, self.default)

     12

     13     def __set__(self, instance, value):

 

TypeError: unhashable type: 'MoProblems'

由於MoProblems的實例(list的子類)是不可哈希的,所以它們不能爲MoProblems.x用作數據字典的key。有一些方法能夠規避這個問題,可是都不完美。最好的方法可能就是給你的描述符加標籤了。

Python

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

class Descriptor(object):

 

    def __init__(self, label):

        self.label = label

 

    def __get__(self, instance, owner):

        print '__get__', instance, owner

        return instance.__dict__.get(self.label)

 

    def __set__(self, instance, value):

        print '__set__'

        instance.__dict__[self.label] = value

 

class Foo(list):

    x = Descriptor('x')

    y = Descriptor('y')

 

f = Foo()

f.x = 5

print f.x

 

__set__

__get__ [] <class '__main__.Foo'>

5

這種方法依賴於Python的方法解析順序(即,MRO)。咱們給Foo中的每一個描述符加上一個標籤名,名稱和咱們賦值給描述符的變量名相同,好比x = Descriptor(‘x’)。以後,描述符將特定於實例的數據保存在f.__dict__['x']中。這個字典條目一般是當咱們請求f.x時Python給出的返回值。然而,因爲Foo.x 是一個描述符,Python不能正常的使用f.__dict__[‘x’],可是描述符能夠安全的在這裏存儲數據。只是要記住,不要在別的地方也給這個描述符添加標籤。

Python

1

2

3

4

5

6

7

8

9

10

11

12

13

14

class Foo(object):

    x = Descriptor('y')

 

f = Foo()

f.x = 5

print f.x

 

f.y = 4    #oh no!

print f.x

__set__

__get__ <__main__.Foo object at 0x10432c810> <class '__main__.Foo'>

5

__get__ <__main__.Foo object at 0x10432c810> <class '__main__.Foo'>

4

我不喜歡這種方式,由於這樣的代碼很脆弱也有不少微妙之處。但這個方法的確很廣泛,能夠用在不可哈希的全部者類上。David Beazley在他的中用到了這個方法。

在元類中使用帶標籤的描述符

因爲描述符的標籤名和賦給它的變量名相同,因此有人使用元類來自動處理這個簿記(bookkeeping)任務。

Python

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

class Descriptor(object):

    def __init__(self):

        #notice we aren't setting the label here

        self.label = None

 

    def __get__(self, instance, owner):

        print '__get__. Label = %s' % self.label

        return instance.__dict__.get(self.label, None)

 

    def __set__(self, instance, value):

        print '__set__'

        instance.__dict__[self.label] = value

 

class DescriptorOwner(type):

    def __new__(cls, name, bases, attrs):

        # find all descriptors, auto-set their labels

        for n, v in attrs.items():

            if isinstance(v, Descriptor):

                v.label = n

        return super(DescriptorOwner, cls).__new__(cls, name, bases, attrs)

 

class Foo(object):

    __metaclass__ = DescriptorOwner

    x = Descriptor()

 

f = Foo()

f.x = 10

print f.x

 

__set__

__get__. Label = x

10

我不會去解釋有關元類的細節——參考文獻中David Beazley已經在他的文章中解釋的很清楚了。 須要指出的是元類自動的爲描述符添加標籤,而且和賦給描述符的變量名字相匹配。

儘管這樣解決了描述符的標籤和變量名不一致的問題,可是卻引入了複雜的元類。雖然我很懷疑,可是你能夠自行判斷這麼作是否值得。

訪問描述符的方法

描述符僅僅是類,也許你想要爲它們增長一些方法。舉個例子,描述符是一個用來回調property的很好的手段。好比咱們想要一個類的某個部分的狀態發生變化時就馬上通知咱們。下面的大部分代碼是用來作這個的:

Python

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

class CallbackProperty(object):

    """A property that will alert observers when upon updates"""

    def __init__(self, default=None):

        self.data = WeakKeyDictionary()

        self.default = default

        self.callbacks = WeakKeyDictionary()

 

    def __get__(self, instance, owner):

        return self.data.get(instance, self.default)

 

    def __set__(self, instance, value):        

        for callback in self.callbacks.get(instance, []):

            # alert callback function of new value

            callback(value)

        self.data[instance] = value

 

    def add_callback(self, instance, callback):

        """Add a new function to call everytime the descriptor updates"""

        #but how do we get here?!?!

        if instance not in self.callbacks:

            self.callbacks[instance] = []

        self.callbacks[instance].append(callback)

 

class BankAccount(object):

    balance = CallbackProperty(0)

 

def low_balance_warning(value):

    if value < 100:

        print "You are poor"

 

ba = BankAccount()

 

# will not work -- try it

#ba.balance.add_callback(ba, low_balance_warning)

這是一個頗有吸引力的模式——咱們能夠自定義回調函數用來響應一個類中的狀態變化,並且徹底無需修改這個類的代碼。這樣作可真是替人分憂解難呀。如今,咱們所要作的就是調用ba.balance.add_callback(ba, low_balance_warning),以使得每次balance變化時low_balance_warning都會被調用。

可是咱們是如何作到的呢?當咱們試圖訪問它們時,描述符老是會調用__get__。就好像add_callback方法是沒法觸及的同樣!其實關鍵在於利用了一種特殊的狀況,即,當從類的層次訪問時,__get__方法的第一個參數是None。

Python

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

class CallbackProperty(object):

    """A property that will alert observers when upon updates"""

    def __init__(self, default=None):

        self.data = WeakKeyDictionary()

        self.default = default

        self.callbacks = WeakKeyDictionary()

 

    def __get__(self, instance, owner):

        if instance is None:

            return self        

        return self.data.get(instance, self.default)

 

    def __set__(self, instance, value):

        for callback in self.callbacks.get(instance, []):

            # alert callback function of new value

            callback(value)

        self.data[instance] = value

 

    def add_callback(self, instance, callback):

        """Add a new function to call everytime the descriptor within instance updates"""

        if instance not in self.callbacks:

            self.callbacks[instance] = []

        self.callbacks[instance].append(callback)

 

class BankAccount(object):

    balance = CallbackProperty(0)

 

def low_balance_warning(value):

    if value < 100:

        print "You are now poor"

 

ba = BankAccount()

BankAccount.balance.add_callback(ba, low_balance_warning)

 

ba.balance = 5000

print "Balance is %s" % ba.balance

ba.balance = 99

print "Balance is %s" % ba.balance

Balance is 5000

You are now poor

Balance is 99

相關文章
相關標籤/搜索