不仅是作用域问题,本质上是闭包的延迟绑定特性(late binding)。
首先看廖大牛的程序,其求质数的原理很简单:
1、通过_odd_iter()内部函数获得奇数生成器。
2、对每个奇数,用比其小的所有奇数去尝试整除,如不能整除,则该奇数为质数。
重点在于第2步的实现,此处用到了一个多层嵌套的filter()函数。
为了看得更明白,我们给程序增加一些输出(详见注释)。
#产生从3开始的奇数生成器
def _odd_iter():
n = 1
while True:
n = n+2
yield n
#判断是否被n整除
def _not_divisible(ii, nn): #函数局部空间变量名改为与传递参数不同,以便区分(不是必须)
return lambda x: print('level%d:%d%%%d' %(ii, x, nn)) or (x % nn > 0)
def primes():
yield 2
it = _odd_iter()
i = 0 #filter层数
while True:
n = next(it)
i += 1
print('=' * 30)
yield n
print('_' * 20)
it = filter(_not_divisible(i, n), it) #对奇数生成器进行筛选
def main():
p = primes()
k = 0
while k < 5:
print('prime-%d:' % k, next(p))
k += 1
if __name__ == '__main__':
main()
以上程序的输出如下,我采用注释方式#给出了输出的详细解释。
prime-0: 2 #yield 2语句输出第一个质数
==============================
prime-1: 3 #第一次执行next(it)后,yield n语句第一次输出
____________________
level 1: 5%3 #第二次执行next(it)时,即第一次调用filter()函数
==============================
prime-2: 5 #yield n语句第二次输出
____________________
level 1: 7%3 #第三次执行next(it),即第二次调用filter()函数(第一层/共二层)
level 2: 7%5 #(第二层/共二层)
==============================
prime-3: 7 #yield n语句第三次输出
____________________
level 1: 9%3 #第三次执行next(it),即第三次调用filter()函数(第一层/共三层),触发过滤条件
level 1: 11%3 #第四次执行next(it),即第四次调用filter()函数(第一层/共三层)(紧接上一个next(),未重新定义it)
level 2: 11%5 #第四次执行next(it),即第四次调用filter()函数(第二层/共三层)
level 3: 11%7 #第四次执行next(it),即第四次调用filter()函数(第三层/共三层)
==============================
prime-4: 11 #yield n语句第四次输出
上面的重点是:
1、执行next(it)语句进行输出时,filter()构成的迭代器才会实现。
2、filter()函数采用了外部函数_not_divisible()作为过滤条件,因此:
(1)由于是外部函数,所以函数参数必须显示传递。
(2)由于是外部函数,其在filter定义时就已经实现,也即已经将primes()函数空间的变量i、n的值的引用传递给_not_divisible()的函数局部变量ii、nn。因此,filter()函数每层的i和n不同。
下面我们将_not_divisible()改为闭包(内嵌函数):
#产生从3开始的奇数生成器
def _odd_iter():
n = 1
while True:
n = n+2
yield n
def primes():
def _not_divisible():
return lambda x: print('level%d:%d%%%d' %(i, x, n)) or (x % n > 0)
yield 2
it = _odd_iter()
i = 0 #filter层数
while True:
n = next(it)
i += 1
print('=' * 30)
yield n
print('_' * 20)
it = filter(_not_divisible(), it) #对奇数生成器进行筛选
def main():
p = primes()
k = 0
while k < 5:
print('prime-%d:' % k, next(p))
k += 1
if __name__ == '__main__':
main()
其输出如下:
prime-0: 2
==============================
prime-1: 3
____________________
level 1: 5%3 #第二次执行next(it)时,即第一次调用filter()函数
==============================
prime-2: 5
____________________
level 2: 7%5 #第三次执行next(it)时,即第二次调用filter()函数(第一层/共二层)
level 2: 7%5 #第三次执行next(it)时,即第二次调用filter()函数(第二层/共二层)
==============================
prime-3: 7
____________________
level 3: 9%7 #第四次执行next(it)时,即第三次调用filter()函数(第一层/共三层)
level 3: 9%7 #第四次执行next(it)时,即第三次调用filter()函数(第二层/共三层)
level 3: 9%7 #第四次执行next(it)时,即第三次调用filter()函数(第三层/共三层)
==============================
prime-4: 9
结合上面_not_divisible()作为外部函数时的重点解释,注意其区别:
1、再次强调,执行next(it)语句进行输出时,filter()构成的迭代器才会实现。
2、由于filter()函数采用了闭包(内嵌函数)作为过滤条件:
(1)作为闭包,变量i、n直接从primes()函数局部空间继承(自由变量),而不是显示定义。
(2)闭包具有延迟绑定特性(closure lazy binding),也即到闭包实现时,才会给其内部的只有变量赋值。针对本例,执行next(it)语句时,filter()迭代器实现,同时其过滤条件_not_divisible()也才实现。因此,filter()函数每层的i和n都一样(都是执行next(it)时的最新值)。
作者采用的匿名函数lambda实际上也构成了一个闭包,其中的参数n作为自由变量,只有在执行next(it)时才会实现,因此与上面的闭包效果一样,无法达到去除合数的效果。
要避免闭包延迟绑定导致的函数参数误用,最简单的方式是采用默认参数:
it = filter(lambda x, y = n: x % y > 0, it)
除此以外,还可采用偏函数、生成器函数等方式避免闭包的延迟绑定。