讓Python中類的屬性具備惰性求值的能力

起步

咱們但願將一個只讀的屬性定義爲 property 屬性方法,只有在訪問它時才進行計算,可是,又但願把計算出的值緩存起來,不要每次訪問它時都從新計算。html

解決方案

定義一個惰性屬性最有效的方法就是利用描述符類來完成它,示例以下:python

class lazyproperty:
    def __init__(self, fun):
        self.fun = fun

    def __get__(self, instance, owner):
        if instance is None:
            return self
        value = self.fun(instance)
        setattr(instance, self.fun.__name__, value)
        return value

要使用這個工具,能夠像下面的方式來使用它:django

class Circle:
    def __init__(self, radius):
        self.radius = radius


    @lazyproperty
    def area(self):
        print('Computing area')
        return 3.1415 * self.radius ** 2

c = Circle(5)
print(c.area)
print(c.area)

能夠看出,這裏的實例方法 area() 只會被調用一次。緩存

爲何會這樣

若是類中定義了 __get__()__set__()__delete__() 中的任何方法,那麼這個就被成爲描述符(descriptor)。函數

通常狀況下(我是說通常狀況下),訪問屬性的默認行爲是從對象的字典中獲取,並沿着一個查找鏈的順序進行搜索,好比對於 a.x 有一個查找鏈,從 a.__dict__['x'] 而後是 type(a).__dict__['x'],再繼續經過 type(a) 的基類開始。工具

而若是查找的值是一個描述符對象,則會覆蓋這個默認的搜索行爲,優先採用描述符的行爲,這個行爲會由於若是調用而有些不一樣。這裏就只說明例子中的狀況。性能

若是描述符綁定的對象實例,a.x 則轉換爲調用: type(a).__dict__['x'].__get__(a, type(a))spa

當一個描述符之定義 __get__() 方法,則它的綁定關係比通常狀況下要弱化不少。特別是,只有當被訪問的屬性不存在對象字典中時,__get__() 纔會被調用。code

更多描述可見文檔:https://docs.python.org/3/ref...htm

這種惰性求值的方法在不少模塊中都會使用,好比django中的 cached_property

20180813175532.png

使用上與例子一致,如表單中的 changed_data :

20180813175928.png

討論

在大部分狀況下,讓屬性具備惰性求值能力的所有意義就在於提高程序性能。當不須要這個屬性時就能避免進行無心義的計算,同時又能阻止該屬性重複進行計算。

本文的技巧中有一個潛在的缺點,就是計算出的值後就變成可變的(mutable)。

>>> c.area
78.53
>>> c.area = 3
>>> c.area
3

若是考慮可變性的問題,可使用另外一種實現方式,但執行效率會稍打折扣:

def lazyproperty(func):
    name = '_lazy_' + func.__name__
    @property
    def lazy(self):
        if hasattr(self, name):
            return getattr(self, name)
        value = func(self)
        setattr(self, name, value)
        return value

    return lazy

若是使用這種方式,就會發現 set 操做是不容許的,全部的 get 操做都必須經由屬性的 getter 函數來處理,這比直接在實例字典中查找相應的值要慢一些。

參考

  1. https://docs.python.org/3/ref...
  2. 《Python Cookbook 第三版》
相關文章
相關標籤/搜索