python 特別的生成器表達式


Ⅰ起因

  學習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之前的代碼以及生成器函數並不會執行,只會進行編譯。

  盡管,生成器表達式代碼更簡潔,但在生成器初始化和生成器調用的效率上都表現出了與傳統生成器函數的差距。

 

 

注:列表推導式並不存在這樣的問題(當然也不應該出現)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM