前一段时间项目上线,太忙了。今天趁着周六,继续Generator的探索之旅。上一篇,我们介绍过了迭代器的概念,很好用。使用了Generator之后,我稍微能触摸到Python的协程与同步编程的概念了。
Python中提供了yield语句,在函数中使用,它可以暂停函数的执行,保存函数的状态,直到下一次调用。这种特性为很多问题提供了一个非常优雅的解决方案。
def gen():
print("Hello, world!")
yield 1
yield 2
yield 3
上面就定义了一个Generator。Generator的定义很简单,与函数的定义非常相似,只是函数体中有yield语句。我们来看看如何使用它:
for i in gen():
print i
输出结果:
Hello, world!
1
2
3
这个是怎么做到的呢?原来通过调用gen(),我们获得了一个Generator对象。在每次循环的时候,都会调用对象的next方法,与前一篇介绍的Iterator一样。我们可以这样来试验一下:
g = gen()
print(g)
打印结果:
object gen at 0x01BBF0F8>
可见我们获得了一个Generator对象,注意这是函数并没有执行!!!
下次我们调用next对象的时候,函数从头开始执行,打印出Hello, world!和1。然后暂停,等待下一次执行。
g.next()
#Hello, world!
#1
g.next()
#2
g.next()
#3
g.next()
#Traceback (most recent call last):
# File "", line 1, in
#StopIteration
现在很清楚了,当函数执行完成会raise StopIteration异常,循环退出。
上面我们说过生成器为很多问题提供了非常优雅的解决方案。假设现在有个需求,打印出2~1千万的素数。我们可以这么写:
import math
def is_prime(num):
square_root = int(math.sqrt(num))
for i in range(2, square_root + 1):
if num % i == 0:
return False
return True
def prime_general(target_num):
result = []
for i in xrange(2, target_num):
if is_prime(i):
result.append(i)
return result
for i in prime_general(10000000):
print(i)
这样写确实能够得到结果,但是会有非常大的内存占用量。因为range(2,10000000)会生成一个1千万的列表。使用Generator可以解决这个问题。因为Generator每次计算一个值,它不会保存所有的计算结果。
def prime_generator(target_num):
for i in xrange(2, target_num):
if is_prime(i):
yield i
for i in prime_generator(10000000):
print(i)
当然有人会说了,上面的例子有点牵强附会,我可以直接这样:
for i in xrange(2, 10000000):
if is_prime(i):
print(i)
这里,我强调一点,算是编程的原则吧。能封装成通用的就封装成通用的。在后面我们会讲到组合多个生成器产生更强大的功能。使用生成器产生素数可以用在其他很多地方,只需要一个循环:
def process(num):
print(num)
for i in prime_generator(100):
process(i)
对应列表推导式,生成器也有一个很简单的推导式,例如上面的实例可以写成:
gen = (i for i in xrange(2, 10000000) if is_prime(i))
for i in gen:
print(i)
Python中还提供了与生成器交互的方法,这里yield是一个表达式,它可以接受外面传入的值。
def generator_send():
while True:
print("Welcome~")
name = (yield)
if name is None:
print("Please get me your name")
else:
print("Hello, " + name)
gen = generator_send()
#无输出
gen.send(None)
#Welcome~
gen.send()
#Please tell me your name
gen.send("guyu")
#Hello, guyu
注意到,我们在generator_send()之后,生成器函数并没有执行。第一次调用send是为了让函数执行到yield等待我们的输入,同时Welcome~被打印出来了。后续调用send,参数被赋值给name对象,执行相应的逻辑。
上面提到过组合生成器的概念,我们来看一个文件过滤的例子,这个在一些服务器log的过滤方面用的很多。例如我们有一个文件log.txt内容如下:
python is beautify.
really?
python is graceful.
yes, I'm sure.
python is powerful.
you can try, you will be amazing.
python has a neat syntax.
it's easy to write.
我们来写一个打印所有行的程序。
def file_line(target):
for line in target:
yield line
def print_line(target):
for line in target:
print(line)
print_line(file_line(open("log.txt")))
仔细观察下,我们发现数据好像是一个流,这个log.text–>file_line–>print_line。其实我们可以组合很多的生成器来实现非常复杂的功能。例如我现在想打印只包含python的行,我可以加一个过滤器:
def filter_python(target):
for line in target:
if "python" in line:
yield line
#这么组合
print_line(filter_python(file_line(open("log.txt"))))
现在只有python的行会被过滤出来了。现在我不想过滤python了,我想过滤出含有is的行,可以再加上一个过滤器:
def filter_is(target):
for line in target:
if "is" is line:
yield line
#组合
print_line(filter_is(file_line(open("log.txt"))))
怎么样?是不是很灵活?Generator很强大,需要很深入的探索才能发现它的价值!
上面我们看到了两种使用方式,一种是数据流的方式,数据从一个生成器流向另一个生成器,这种情况都是有一个固定的数据源。另一种则是主程序驱动的方式,通过send驱动,通过程序发送数据给生成器,驱动它们一步步前进。这两种方案存在细微的差异,建议不用混合使用,否则会造成难以排查的bug。
def countdown(n):
print "Counting down from ", n
while n >= 0:
newvalue = (yield n)
if newvalue is not None:
n = newvalue
else:
n -= 1
c = countdown(5)
for n in c:
print(n)
if n == 5:
c.send(3)
#结果:
#Counting down from 5
#5
#2
#1
#0
我们来分析一下,为什么会出现这样的结果。第一次for循环,countdown打印Counting down from 5,暂停于yield处,并且返回5,for循环中打印5。但是,这之后调用了一次send(3),这回countdown从yield处返回,获得新值3,赋值给n,n这时为3。继续执行,暂停与下一次yield。第三次for循环,调用next之后yield从上一次暂停中继续,会走到n-=1,这时n为2。后面的我相信都顺理成章了!
Python Generator是一个很强大的特性,需要在使用中仔细揣摩。多练习方能熟能生巧。