产生器 generator
当 Python 的程序需要迭代内容非常大的串列时,往往会消耗不少电脑的内存,这时如果改用“产生器”的方式,就能产生更好的效能,这篇教学将会介绍 Python 的产生器 ( 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)
如果将 print 的部分换成 yield,印出结果就会看见已经变成 generator。
def f(max):
n = 0
a = 2
while n<max:
yield(a) # 換成 yield
a = a ** 2
n = n + 1
f(5)
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]
使用产生器找质数
如果要快速找出一群数字里的质数,可以使用“埃拉托斯特尼筛法”来寻找,埃拉托斯特尼筛法的原理就是“依序将找到的质数的倍数剔除”,因此每次找到质数之后,要寻找的数字就会变少,所以可以快速找出质数。
根据这个法则,可以简单撰写出下方的程序码,在尚未使用 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)
如果改成 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)) # 印出結果
微信扫码关注
抖音扫码关注