Ⅰ起因
学习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之前的代码以及生成器函数并不会执行,只会进行编译。
尽管,生成器表达式代码更简洁,但在生成器初始化和生成器调用的效率上都表现出了与传统生成器函数的差距。
注:列表推导式并不存在这样的问题(当然也不应该出现)