iT邦幫忙

2023 iThome 鐵人賽

DAY 17
0
Software Development

跟著 OXXO 一起學 Python系列 第 34

( Day 17.1 ) Python 產生器 generator

  • 分享至 

  • xImage
  •  

當 Python 的程式需要迭代內容非常大的串列時,往往會消耗不少電腦的記憶體,這時如果改用「產生器」的方式,就能產生更好的效能,這篇教學將會介紹 Python 的產生器 ( generator )。

原文參考:產生器 generator

本篇使用的 Python 版本為 3.7.12,所有範例可使用 Google Colab 實作,不用安裝任何軟體 ( 參考:使用 Google Colab )

什麼是產生器 generator?

產生器是一個 Python 序列製作物件,可以用它來迭代一個可能很大的序列,在迭代的過程中所產生的值都是動態的,不需要將整個序列儲存在記憶體中

產生器的特性

  • 產生器是記錄「產生值的方法」,而不是記錄值。
  • 使用產生器中「產生的值只能取用一次」,無法重新啟動或重新取得 ( 因為不會紀錄 )。

產生器表示式

在「生成式」的文章裡有介紹過串列的生成式,而產生器表示式跟生成式的格式很像,差別在於將中括號「[]」改成小括號「()」,執行後可以看到產生器會回傳一個產生器物件 object,而不是串列。

a = [i for i in range(10)]  # 串列生成式
b = (i for i in range(10))  # 產生器表示式
print(a)   # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(b)   # <generator object <genexpr> at 0x7fbb6facba50>

和串列相同,也可以使用類似 for 迴圈的方式取出產生器的值,但所有的值都只能取出一次,以下方的程式為例,如果是串列生成式,因為記憶體中保留了整份串列,所以再次取值時還是能得到數值,如果是使用產生器表示式,再次取值時,就完全取不到值。

a = [i**2 for i in range(10)]
for i in a:
    print(i, end=' ')    # 0 1 4 9 16 25 36 49 64 81
for i in a:
    print(i, end=' ')    # 0 1 4 9 16 25 36 49 64 81
print()
b = (i**2 for i in range(10))
for i in b:
    print(i, end=' ')    # 0 1 4 9 16 25 36 49 64 81
for i in b:
    print(i, end=' ')    # 取不到值

此外,也可使用「next」的方法依序取值,但如果最後取不到值就會發生錯誤。

a = (i**2 for i in range(10))  # 串列生成式
print(next(a))  # 0
print(next(a))  # 1
print(next(a))  # 4
print(next(a))  # 9
print(next(a))  # 16
print(next(a))  # 25
print(next(a))  # 36
print(next(a))  # 49
print(next(a))  # 64
print(next(a))  # 81
print(next(a))  # 發生錯誤,因為取不到值

yield 陳述式

如果一個函式裡,包含 yield 陳述式,那麼這個函式就會變成一個產生器 ( generator 函式 ),舉例來說,下方的程式是一個基本的函式,執行後會依序印出對應的數字。

def f(max):
    n = 0
    a = 2
    while n<max:
        print(a)
        a = a ** 2
        n = n + 1
f(5)

Python 教學 - 產生器 generator - yield 陳述式

如果將 print 的部分換成 yield,印出結果就會看見已經變成 generator。

def f(max):
    n = 0
    a = 2
    while n<max:
        yield(a)     # 換成 yield
        a = a ** 2
        n = n + 1
f(5)

Python 教學 - 產生器 generator - yield 陳述式

generator 函式和普通函式的執行流程不同。普通函式是順序執行,遇到 return 語句就會返回。而 generator 函式會在每次調用 next() 的時候執行,遇到 yield 語句返回,再次執行時從上次返回的 yield 語句處繼續執行

舉例來說,下方程式碼是一個普通函式,呼叫函式執行後,會一次印出 1、2、3。

def f():
    print(1)
    print(2)
    print(3)
f()    # 一次印出 1、2、3

如果使用 yield 將其變成產生式,就需要使用 next 或是迴圈方式調用,每次呼叫時才會逐步印出 1、2、3

def f():
    yield(1)       # 使用 yield
    yield(2)
    yield(3)
g = f()          # 賦值給變數 g
print(next(g))   # 1
print(next(g))   # 2
print(next(g))   # 3

為什麼上方的程式碼要使用「g = f()」呢?因為調用 generator 函式會建立一個 generator 物件,多次調用generator 函式會創建多個「相互獨立」的 generator,如果將程式碼改成下面的模樣,因為 generator 函式互相獨立,結果就只會印出 1。

def f():
    yield(1)
    yield(2)
    yield(3)
print(next(f()))   # 1
print(next(f()))   # 1
print(next(f()))   # 1

下方的程式碼,使用 for 迴圈依序取出 generator 函式所運算的數值,並將數值分別放入兩個串列當中。

def f(max):
    n = 0
    while n<max:
        yield(n)
        n = n + 1
g = f(10)
a = []
b = []
for i in range(5):
    a.append(next(g))
for i in range(5):
    b.append(next(g))
print(a)     # [0, 1, 2, 3, 4]
print(b)     # [5, 6, 7, 8, 9]

使用產生器找質數 {a4}

如果要快速找出一群數字裡的質數,可以使用「埃拉托斯特尼篩法」來尋找,埃拉托斯特尼篩法的原理就是「依序將找到的質數的倍數剔除」,因此每次找到質數之後,要尋找的數字就會變少,所以可以快速找出質數。

根據這個法則,可以簡單撰寫出下方的程式碼,在尚未使用 generator 函式的時候,必須要一個一個依序撰寫,在數字量大的時候相當沒有效率。

a = range(2,100)                            # 產生 2~100 的串列
print(*a)
b = [i for i in a if i==a[0] or i%a[0]>0]   # 找出第一個質數,並將串列裡該質數的倍數剔除
print(*b)
c = [i for i in b if i==b[1] or i%b[1]>0]   # 找出第二個質數,並將串列裡該質數的倍數剔除
print(*c)
d = [i for i in c if i==c[2] or i%c[2]>0]   # 找出第三個質數,並將串列裡該質數的倍數剔除
print(*d)

Python 教學 - 產生器 generator - 埃拉托斯特尼篩法找質數

如果改成 generator 函式,就可以比較輕鬆的找出全部的質數。

def gg(max):                   # 定義一個 gg 函式
    s = set()                    # 設定一個空集合
    for n in range(2,max):       # 從 range(2, max) 當中開始依序找質數
        if all(n%i>0 for i in s):  # 判斷如果 i 已經存在於集合,且除以集合中的值會有餘數 ( 整除表示非質數 )
            s.add(n)                 # 將該數字加入集合 ( 表示質數 )
            yield n                  # 使用 yield 記錄狀態
print(*gg(100))                # 印出結果

Python 教學 - 產生器 generator - 使用產生器找質數

更多教學

大家好,我是 OXXO,是個即將邁入中年的斜槓青年,我有個超過一千篇教學的 STEAM 教育學習網,有興趣可以參考下方連結呦~ ^_^


上一篇
( Day 16.2 ) Python 閉包 ( Closure )
下一篇
( Day 17.2 ) Python 裝飾器 decorator
系列文
跟著 OXXO 一起學 Python101
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言