DEV Community

codemee
codemee

Posted on

生成器函式和傳回生成器的函式到底有什麼不一樣?

生成器函式 (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))
Enter fullscreen mode Exit fullscreen mode

f2 只是 f1 的另外一種寫法, 所以兩個函式使用上是一樣的:

>>> for i in f1(2):
...     print(i)
0
1
>>> for i in f2(2):
...     print(i)
0
1
Enter fullscreen mode Exit fullscreen mode

甚至也都可以個別先取得函式傳回值, 然後再取值:

>>> g1 = f1(2)
>>> g2 = f2(2)
>>> for i in g1:
...     print(i)
0
1
>>> for i in g2:
...     print(i)
0
1
Enter fullscreen mode Exit fullscreen mode

在程式中區別兩種函式

這兩個函式因為實作上的差別, f1 稱為生成器函式 (generator function), 而 f2 則是一個普通的函式, 只是它的傳回值是生成器而已。如果想要在程式中區別這兩種函式, 可以透過 inspact 模組的 isgeneratorfunction 函式, 例如:

>>> inspect.isgeneratorfunction(f1)
True

>>> inspect.isgeneratorfunction(f2)
False
Enter fullscreen mode Exit fullscreen mode

但如果是由函式的傳回值來區別, 則兩個傳回值一樣都是生成器:

>>> inspect.isgenerator(g1)
True

>>> inspect.isgenerator(g2)
True
Enter fullscreen mode Exit fullscreen mode

有些模組在需要函式的地方會強制要求要傳入生成器函式, 而不是傳回生成器的函式, 使用時要多加留意。

真正的差別--生成器函式採 lazy evaluation

除了稱呼上的差別外, 這兩者其實有一個最重要的差異, 就是生成器函式採用懶惰式的執行方式 (lazy evaluation), 也就是除非利用 next() 或是 for 取值, 才會繼續執行函式內的程式碼, 否則並不會繼續執行。舉例來說, 在 f1f2 中都有檢查傳入參數是否為負數的機制, 照理說只要傳入負數就應該要引發例外, 以下是兩個函式的執行結果:

>>> g1 = f1(-1)
>>> g2 = f2(-1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in f2
Exception: 不能是負數
Enter fullscreen mode Exit fullscreen mode

你會發現傳入負數給 f1 不會出事, 但是 f2 就會立即引發例外。這就是因為 f1 是生成器函式, 只是單純叫用它它只會等在那邊, 並不會真的往下執行, 所以不會引發例外。等到我們要它吐值出來時, 才會往下執行到 yield

>>> next(g1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in f1
Exception: 不能是負數
Enter fullscreen mode Exit fullscreen mode

這時才會因為檢查發現傳入的是負數而引發例外。而一旦執行到 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: 不能是負數
Enter fullscreen mode Exit fullscreen mode

明明都用 try...except 捕捉例外了, 為什麼還會引發例外?

Top comments (0)