生成器函式 (generator function)和傳回生成器的函式看起來很像, 使用起來也非常像, 但是他們到底有什麼不一樣呢?請看以下兩個函式:
>>> def f1(n):
... if n < 0:
... raise Exception('不能是負數')
... for i in range(n):
... yield i
>>> def f2(n):
... if n < 0:
... raise Exception('不能是負數')
... return (i for i in range(n))
f2
只是 f1
的另外一種寫法, 所以兩個函式使用上是一樣的:
>>> for i in f1(2):
... print(i)
0
1
>>> for i in f2(2):
... print(i)
0
1
甚至也都可以個別先取得函式傳回值, 然後再取值:
>>> g1 = f1(2)
>>> g2 = f2(2)
>>> for i in g1:
... print(i)
0
1
>>> for i in g2:
... print(i)
0
1
在程式中區別兩種函式
這兩個函式因為實作上的差別, f1
稱為生成器函式 (generator function), 而 f2
則是一個普通的函式, 只是它的傳回值是生成器而已。如果想要在程式中區別這兩種函式, 可以透過 inspact
模組的 isgeneratorfunction
函式, 例如:
>>> inspect.isgeneratorfunction(f1)
True
>>> inspect.isgeneratorfunction(f2)
False
但如果是由函式的傳回值來區別, 則兩個傳回值一樣都是生成器:
>>> inspect.isgenerator(g1)
True
>>> inspect.isgenerator(g2)
True
有些模組在需要函式的地方會強制要求要傳入生成器函式, 而不是傳回生成器的函式, 使用時要多加留意。
真正的差別--生成器函式採 lazy evaluation
除了稱呼上的差別外, 這兩者其實有一個最重要的差異, 就是生成器函式採用懶惰式的執行方式 (lazy evaluation), 也就是除非利用 next()
或是 for
取值, 才會繼續執行函式內的程式碼, 否則並不會繼續執行。舉例來說, 在 f1
和 f2
中都有檢查傳入參數是否為負數的機制, 照理說只要傳入負數就應該要引發例外, 以下是兩個函式的執行結果:
>>> g1 = f1(-1)
>>> g2 = f2(-1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in f2
Exception: 不能是負數
你會發現傳入負數給 f1
不會出事, 但是 f2
就會立即引發例外。這就是因為 f1
是生成器函式, 只是單純叫用它它只會等在那邊, 並不會真的往下執行, 所以不會引發例外。等到我們要它吐值出來時, 才會往下執行到 yield
:
>>> next(g1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in f1
Exception: 不能是負數
這時才會因為檢查發現傳入的是負數而引發例外。而一旦執行到 yield
處, 就一樣會賴在哪裡不動, 等下一次取值才會再往下執行。
如果不瞭解這一點, 就可能會以為遇到靈異現象, 例如以下的程式碼:
>>> try:
... g = f1(-1)
... except:
... print('Oops....')
... else:
... next(g)
Traceback (most recent call last):
File "<stdin>", line 6, in <module>
File "<stdin>", line 3, in f1
Exception: 不能是負數
明明都用 try...except
捕捉例外了, 為什麼還會引發例外?
Top comments (0)