8.10 使用延遲計(jì)算屬性

2018-02-24 15:26 更新

問題

你想將一個(gè)只讀屬性定義成一個(gè)property,并且只在訪問的時(shí)候才會計(jì)算結(jié)果。但是一旦被訪問后,你希望結(jié)果值被緩存起來,不用每次都去計(jì)算。

解決方案

定義一個(gè)延遲屬性的一種高效方法是通過使用一個(gè)描述器類,如下所示:

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

    def __get__(self, instance, cls):
        if instance is None:
            return self
        else:
            value = self.func(instance)
            setattr(instance, self.func.__name__, value)
            return value

你需要像下面這樣在一個(gè)類中使用它:

import math

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

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

    @lazyproperty
    def perimeter(self):
        print('Computing perimeter')
        return 2 * math.pi * self.radius

下面在一個(gè)交互環(huán)境中演示它的使用:

>>> c = Circle(4.0)
>>> c.radius
4.0
>>> c.area
Computing area
50.26548245743669
>>> c.area
50.26548245743669
>>> c.perimeter
Computing perimeter
25.132741228718345
>>> c.perimeter
25.132741228718345
>>>

仔細(xì)觀察你會發(fā)現(xiàn)消息 Computing areaComputing perimeter 僅僅出現(xiàn)一次。

討論

很多時(shí)候,構(gòu)造一個(gè)延遲計(jì)算屬性的主要目的是為了提升性能。例如,你可以避免計(jì)算這些屬性值,除非你真的需要它們。這里演示的方案就是用來實(shí)現(xiàn)這樣的效果的,只不過它是通過以非常高效的方式使用描述器的一個(gè)精妙特性來達(dá)到這種效果的。

正如在其他小節(jié)(如8.9小節(jié))所講的那樣,當(dāng)一個(gè)描述器被放入一個(gè)類的定義時(shí),每次訪問屬性時(shí)它的 __get__() 、__set__()__delete__() 方法就會被觸發(fā)。不過,如果一個(gè)描述器僅僅只定義了一個(gè) __get__() 方法的話,它比通常的具有更弱的綁定。特別地,只有當(dāng)被訪問屬性不在實(shí)例底層的字典中時(shí) __get__() 方法才會被觸發(fā)。

lazyproperty 類利用這一點(diǎn),使用 __get__() 方法在實(shí)例中存儲計(jì)算出來的值,這個(gè)實(shí)例使用相同的名字作為它的property。這樣一來,結(jié)果值被存儲在實(shí)例字典中并且以后就不需要再去計(jì)算這個(gè)property了。你可以嘗試更深入的例子來觀察結(jié)果:

>>> c = Circle(4.0)
>>> # Get instance variables
>>> vars(c)
{'radius': 4.0}

>>> # Compute area and observe variables afterward
>>> c.area
Computing area
50.26548245743669
>>> vars(c)
{'area': 50.26548245743669, 'radius': 4.0}

>>> # Notice access doesn't invoke property anymore
>>> c.area
50.26548245743669

>>> # Delete the variable and see property trigger again
>>> del c.area
>>> vars(c)
{'radius': 4.0}
>>> c.area
Computing area
50.26548245743669
>>>

這種方案有一個(gè)小缺陷就是計(jì)算出的值被創(chuàng)建后是可以被修改的。例如:

>>> c.area
Computing area
50.26548245743669
>>> c.area = 25
>>> c.area
25
>>>

如果你擔(dān)心這個(gè)問題,那么可以使用一種稍微沒那么高效的實(shí)現(xiàn),就像下面這樣:

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

如果你使用這個(gè)版本,就會發(fā)現(xiàn)現(xiàn)在修改操作已經(jīng)不被允許了:

>>> c = Circle(4.0)
>>> c.area
Computing area
50.26548245743669
>>> c.area
50.26548245743669
>>> c.area = 25
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
>>>

然而,這種方案有一個(gè)缺點(diǎn)就是所有g(shù)et操作都必須被定向到屬性的 getter 函數(shù)上去。這個(gè)跟之前簡單的在實(shí)例字典中查找值的方案相比效率要低一點(diǎn)。如果想獲取更多關(guān)于property和可管理屬性的信息,可以參考8.6小節(jié)。而描述器的相關(guān)內(nèi)容可以在8.9小節(jié)找到。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號