本系列文章一些重要的函式、方法、類我都實現的一遍,你可以在github(點選此處)中找到程式碼和測試例子(如果網速過慢我也放了一份在gitee(點選此處)上,但請勿在gitee上提issue
或者留言),歡迎star
/fork
。
緣起
我們回到介紹高階函式的一章,我們提到了高階函式特別是科裡化的一個好處便是「提前求值」和「推遲求值」,通過這些操作,我們可以大大優化很多程式碼。比如,我們使用之前的例子:
def f(x): # x儲存了某種我們需要的狀態
## 所有可以提前計算的放在這裡
z = x ** 2 + x + 1
print('z is {}'.format(z))
def helper(y):
## 所有延遲計算的放在這裡
return y * z
return helper
我們在呼叫f(1)
的時候,其實就已經事先計算了z
的部分,如果我們臨時儲存這個值,反覆呼叫時就可以節省很大的時間:
>>> g = f(1)
z is 3
>>> g(2) + g(1) # 可以看到這次就不會列印`z is xxxx`的輸出了
9
也就是說適時的「提前求值」和「推遲求值」都可以幫助我們大大地減少很多運算開銷。這就引入我們這一篇要講的「惰性求值」的概念,惰性求值的概念主要是:呼叫時才計算,並且只計算一次。
惰性屬性與惰性值
我們考慮下面一個例子:
定義一個圓的類,通過圓心和半徑來描述,但是當我們知道圓心和半徑之後我們能知道很多事,比如:
- 周長(
perimeter
) - 面積(
area
) - 圓最上面座標的位置(
upper_point
) - 圓心到原點的距離(
distance_from_origin
) - ...
這個列表可能非常非常多,而且隨著軟體功能的增加,這個列表可能還會新增。我們可能有兩種方法實現。第一種就是在初始化的時候都給設定為圓的屬性:
@dataclass
class CircleInitial:
x: float
y: float
r: float
def __init__(self, x, y, r):
self.x = x
self.y = y
self.r = r
self.perimeter = 2 * r
self.area = r * r * 3.14
self.upper_point = (x, y + r)
self.lower_point = (x, y - r)
self.left_point = (x - r, y)
self.right_point = (x + r, y)
self.distance_from_origin = (x ** 2 + y ** 2) ** (1/2)
我們馬上可以看出問題:如果這樣的屬性非常多,而且涉及的計算也非常多的話,那麼當我們例項化一個新的物件的時候,耗費的時間將會非常長。然而,大部分的屬性,我們可能都不會用到。
於是,就有了第二個方案,把這些實現成一個方法(我們這裡僅舉例一個area
方法):
@dataclass
class CircleMethod:
x: float
y: float
r: float
def area(self):
print("area calculating...")
return self.r * self.r * 3.14
當然,因為這個值是一個「常」量的概念,我們也可以使用property
修飾器,這樣我們就可以不用帶括號地呼叫它了:
@dataclass
class CircleMethod:
x: float
y: float
r: float
@property
def area(self):
print("area calculating...")
return self.r * self.r * 3.14
我故意在其中加入了一行列印程式碼,我們可以發現,我們每次呼叫area
時,都會被計算一次:
>>> a = CircleMethod(1, 2, 3)
>>> a.area ** 2 + a.area + 1
area calculating...
area calculating...
827.8876000000001
這又是另外一種浪費了,於是我們發現,第一種方案適合需要經常被反覆呼叫的屬性,第二個方案實現很少被呼叫的屬性。但是,可能我們在維護程式碼的時候,沒法事先預判一個屬性是不是經常被呼叫,而且這也不是一個長久之計。但我們發現我們需要的就是那麼一個屬性:
- 這個屬性不會初始化的時候計算
- 這個屬性只在被呼叫時計算
- 這個屬性只會計算一次,後面不會呼叫
這個就是「惰性求值」的概念,我們也把這種屬性叫「惰性屬性」。Python
沒有內建的惰性屬性的概念,不過,我們可以很容易從網上找到一個實現(你也可以在我的Python-functional-programming
中的lazy_evaluate.py
中找到):
def lazy_property(func):
attr_name = "_lazy_" + func.__name__
@property
def _lazy_property(self):
if not hasattr(self, attr_name):
setattr(self, attr_name, func(self))
return getattr(self, attr_name)
return _lazy_property
具體的使用,只是切換一下修飾器property
:
@dataclass
class Circle:
x: float
y: float
r: float
@lazy_property
def area(self):
print("area calculating...")
return self.r * self.r * 3.14
我們採用和上面一樣的呼叫方式,可以發現,area
只計算了一次(只列印了一次):
>>> b = Circle(1, 2, 3)
>>> b.area ** 2 + b.area + 1
area calculating...
827.8876000000001
同樣的理由我們也可以實現一個惰性值的概念,不過因為python
沒有程式碼塊的概念,我們只能用沒有引數的函式來實現:
class _LazyValue:
def __setattr__(self, name, value):
if not callable(value) or value.__code__.co_argcount > 0:
raise NotVoidFunctionError("value is not a void function")
super(_LazyValue, self).__setattr__(name, (value, False))
def __getattribute__(self, name: str):
try:
_func, _have_called = super(_LazyValue, self).__getattribute__(name)
if _have_called:
return _func
else:
res = _func()
super(_LazyValue, self).__setattr__(name, (res, True))
return res
except:
raise AttributeError(
"type object 'Lazy' has no attribute '{}'"
.format(name)
)
lazy_val = _LazyValue()
具體呼叫方法如下,如果你要設計一個模組而這個變數不在類中,那麼就可以很方便地使用它了:
def f():
print("f compute")
return 12
>>> lazy_val.a = f
>>> lazy_val.a
f compute
12
>>> lazy_val.a
12
惰性迭代器/生成器
此外,Python
內建了一些惰性的結構主要就是迭代器和生成器,我們可以很方便驗證它們只計算/保留一次(這裡只驗證迭代器):
>>> a = (i for i in range(5))
>>> list(a)
[0, 1, 2, 3, 4]
>>> list(a)
[]
我們可以設計下面兩個函式:
def f(x):
print("f")
return x + 1
def g(x):
print("g")
return x + 1
然後我們思考下面的結果:
>>> a = (g(i) for i in (f(i) for i in range(5)))
>>> next(a)
它可能有兩種結果,一個它可能的計算方式是這樣的:
>>> temp = [f(i) for i in range(5)]
>>> res = g(temp[0])
如果是這種結果,則它會列印出5個f
然後再列印出g
另一種可能性則是:
>>> res = (g(f(i)) for i in range(5))
則,這樣子便只會列印一個f
和一個g
。如果根據惰性求值的定義,i=1
並沒有被真實呼叫,所以它應該不用求值,所以,如果他符合第二個列印情況,則它就是惰性的物件。事實也就真如此。
當然,這個特性已經非常的Fancy了,但是我們基於此可以聯想出的一個非常奇妙的引用,因為在迭代器計算中,我們並不是在生成的時候,就計算出了迭代器中的每個值,因此,我們可以用這個方式儲存一個無窮系列。通過上面的方式計算後返回結果。一個最簡單的例子是內建模組中的itertools.repeat
,我們可以生成一個無窮的全為1
的線性結構:
from itertools import repeat
repeat_1 = repeat(1)
這樣,我們就可以用上面的列表表示式來做一些計算再通過next
呼叫了。
res = (g(i) for i in (i * 3 for i in repeat_1))
next(res)
我們也將這些線性結構稱為「惰性列表」(這裡的repeat_1
則是一個「無窮惰性列表」的例子),在下面的文章中,我們將詳細地用這個方式來完成一些有趣的事情。