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 捕捉例外了, 為什麼還會引發例外?

Reinvent your career. Join DEV.

It takes one minute and is necessary in the AI era.

Get started

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs