Ⅰ起因
學習python的同學通常會遇到這樣一道經典生成器測試題:
def gen(): for i in range(4): yield i base = gen() for n in (2,10): base = (i+n for i in base) print(list(base))

[21,22,23,24] #簡單解答: 因為for循環了兩次,並對base從新賦值了,所以可以簡化為(i+n for i in (i+n for i in base)) 而n 全部引用了后賦值的10。最里面的base引用的是gen。
但是這個解答並沒有回答一個核心問題:為什么最里層的n 始終用的是10,而base可以找到之前的gen()?
為了簡化問題,我把這道題改造了成這樣:
a1 = 3 b = (i for i in range(a1)) a1 = 5 list(b) #[0, 1, 2] a1 = 3 b = (a1 for i in range(3)) a1= 5 list(b) #[5, 5, 5]
或許各位會猜測:這個問題可能和for后面的數據類型有關系吧?
Ⅱ原理探索
但如果把range()和前面的數值都改造為列表,結果如下:
a1 = 1 b=([a1,] for i in range(3)) a1=2 list(b) # [[2], [2], [2]] a1 =1 b = (i for i in [a1,]) a1 = 2 list(b) # [1] #也可以把以上兩個表達式結合一下 a1 = 1 b = ([a1,i] for i in [a1,]) a1 = 2 list(b) #[[2, 1]]
顯而易見,當變量在for前面的時候,會引用后聲明的值,而當變量在for后面的iterator中的時候會引用之前聲明的值,並且與數據類型無關。
By the way, 可能很人多還不確定列表本身的設定:a =1 b = [a,] a =2 print(b) #[1,]
當然以上全部是生成器表達式。如果手動定義一下生成器呢?
a =1 def zz(): for i in [a,]: yield [a,i] cc = zz() a=2 print(list(cc)) #[[2, 2]] #如果傳入a a =1 def zz(a): for i in [a,]: yield [a,i] cc = zz(a) a=2 print(list(cc)) #[[1,1]]
生成器函數的測試結果是前后一致,不存在這個問題。
進一步測試:
a = 1 c = ([b,i] for i in [a,]) b = 1 list(c) #[[1, 1]] # 但是如果a在生成器表達式后面定義的話: c = ([b,i] for i in [a,]) b = 1 a = 1 list(c) # 會報錯
#p.s. 在生成器函數也不會報錯
Ⅲ執行效率比較
對於簡單的生成器,生成器表達式更方便、更直觀。那么兩者的執行效率是否存在差異呢?Timeit!

import timeit def b(): a = 9999 def c(): for i in range(a): yield i list(c()) print(timeit.timeit(stmt=b,number=1000))

import timeit def b(): a = 9999 c = (i for i in range(a)) list(c) print(timeit.timeit(stmt=b,number=1000))
結果:
函數模式 表達式模式
1.260876 1.235369
1.253225 1.238639
1.256804 1.235393
1.258575 1.238165
我們看到生成器表達式提供的便利的確是以效率的損耗作為代價的。
進一步的驗證表明:生成器表達式初始化的過程相比生成器函數需要花費更多的時間(接近2倍),但是由於初始化的時間過短,並不是形成差距的主要原因。函數模式的生成器會隨着next()次數的增加在時間上逐步拉開與生成器表達式差距。調用效率的差距才是主要原因。
Ⅳ結論
生成器表達式,會在程序執行的過程中運行for 后面的代碼,並對for后面的代碼進行賦值,而for之前的代碼以及生成器函數並不會執行,只會進行編譯。
盡管,生成器表達式代碼更簡潔,但在生成器初始化和生成器調用的效率上都表現出了與傳統生成器函數的差距。
注:列表推導式並不存在這樣的問題(當然也不應該出現)