Python Generator初探(二)

  • 前言
  • 生成器
    • 概念
    • 实例用法
    • 生成器推导式
    • 与生成器交互
  • 生成器高级用法
  • 使用建议
  • 总结

前言

前一段时间项目上线,太忙了。今天趁着周六,继续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是一个很强大的特性,需要在使用中仔细揣摩。多练习方能熟能生巧。

你可能感兴趣的:(Python)