生成器与yield关键字

1.可迭代对象

为了说明可迭代对象,首先我们要知道,迭代的概念。我们先来看一个实例:

ls = [1,2,3,4,5]
for i in ls:
    print(i)

上面的实例非常简单,我们创建了一个列表ls,并且用for语句遍历这个列表的每一个元素。这里列表ls被遍历的这个行为,就称之为迭代。

 

明白了迭代的概念之后,你就会发现,在Python中,能够被迭代的不只有列表,例如你还可以迭代字符串,元组,文件,字典等等,这些能够被迭代的对象,就称作可迭代对象。

2.生成器

迭代固然是处理大量数据的好方法。但是以列表为例,迭代存在两个问题,第一,如果列表中的元素太多了,将大量占用内存。第二,我们有时候只需要使用一次数据,如果用列表把数据全部保存起来,岂不是有些浪费?Python中的生成器就能很好的解决这两个问题。

 

2.1.生成器函数

生成器是一种可以简单有效的创建迭代器的工具。它们像常规函数一样撰写,但是在需要返回数据时使用yield语句。每当对它调用next()函数(有关next函数下面会提及),生成器从它上次停止的地方重新开始(它会记住所有的数据值和上次执行的语句)。

上面是我在Python官方文档中找到的定义,下面以实例作为解释:

def print_letter(data):
    for index in range(len(data)):
        yield data[index]
g = print_letter("hello")
for i in g:
    print(i)

等价于

def print_letter(data):
    for index in range(len(data)):
        yield data[index]
for i in print_letter("hello"):
    print(i)

打印结果:
h
e
l
l
o

上述代码中,def部分定义了一个生成器函数,print_letter("hello")这行代码将返回一个生成器。为了理解生成器(即def定义的部分),我们可以从函数的角度理解,当调用函数时,代码是按顺序结构执行的,生成器与函数的区别在于,函数遇到return返回,而生成器执行到yield时返回yield之后的语句;另外,函数返回时会释放内部定义的变量,而生成器则会保持退出时的状态。以上面代码为例,如果print_letter是函数,那么只返回一次数据(将yield改成return,也就是返回data[0]);但对于生成器,它将在一次调用后接着进入之前的状态,执行代码,每次都返回yield后的语句,直到不再满足条件时停止(对于这里的例子,yield第一次返回data[0],第二次返回data[1],以此类推,当不再满足for遍历条件时就不再返回yield后的语句了)。除了使用for遍历以外,生成器可以不断调用next()函数来实现“遍历”,另外需要指出的是,如果你用next函数遍历完生成器后,程序将会抛出一个StopIteration的异常。

如果使用type函数查看print_letter的类型,可以验证它是生成器(generator)

...type(print_letter)
>>>function
...type(print_letter('hello'))
>>>generator

2.2.生成器表达式

除了像函数那样定义一个生成器之外,还有一种定义生成器的简单方法。即生成器表达式。

 

介绍生成器表达式之前我们先介绍一下和它长的很像的列表生成式(List Comprehension),看下面实例:

# 两种方式产生列表[1,4,9,16]
ls = []
for i in range(1,5):
    ls.append(i*i)

等价于

ls = [i**i for i in range(1,5)]   # 列表生成式

通过实例可以看出,列表生成式是一种定义列表的简洁方法,实际中也推荐大家使用,这种写法更加pythonic。知道了列表生成式,就很容易得到生成器表达式了。

以上面的代码为例,要把列表生成式修改成生成器表达式,只需要把[]改为(),即

>>>g = (i**i for i in range(1,5))  # 生成器表达式
>>>print(type(g))

<class 'generator'>

可见,构建一个生成器有两种方式,1.生成器函数 2.生成器表达式 。按照实际情况选择。

3.yield关键字

如果你理解了我前面所说的生成器函数运作机制,那么yield关键词你应该也懂了。

再次概括的话就是:生成器内部的代码执行到yield会返回,返回的内容为yield后的表达式。下次再执行生成器的内部代码时将从上次的状态继续开始。通过yield关键字,我们可以很方便的将一个函数修改为生成器。

4.生成器的实际运用

你可能会问,生成器到底能干啥?下面我贴上部分爬取猫眼电影top100的实战代码,希望对你有所启发。

# 提取目标内容并且格式化
def parse_one_page(html):   
    pattern = re.compile('<dd>.*?index.*?>(.*?)</i>.*?<img data-src="(.*?)".*?</a>.*?name"><a.*?>(.*?)</a>.*?star">(.*?)</p>.*?releasetime">(.*?)</p>.*?integer">(.*?)</i>.*?fraction">(.*?)</i>',re.S)
    items = re.findall(pattern, html)
    for item in items:
        yield {     # 每次被调用返回yield后面的参数 这里是一个字典(代表一条电影信息)
                'index' : item[0],
                'image' : item[1],
                'title' : item[2].strip(),
                'actor' : item[3].strip()[3:] if len(item[3]) > 3 else '',
                'time'  : item[4].strip()[5:] if len(item[4]) > 5 else '',
                'score' : item[5].strip() + item[6].strip()
        }
# 爬取一个网页
def main(offset):
    url = 'http://maoyan.com/board/4?offset='+str(offset)
    html = get_one_page(url)
    for item in parse_one_page(html):   # Parse_one_page(html)是一个个可迭代对象 实质上是个生成器 
        print(item)
        write_to_file(item)

5.总结

一开始我们介绍了迭代的概念,从迭代的问题引出了生成器的概念,阐述了生成器的运行机制,介绍了两种构建生成器的方式(生成器函数以及生成器表达式),解释了yield关键字的作用,最后我们通过实战代码见识了生成器在实际项目中作用。

posted @ 2019-12-07 19:17  blueattack  阅读(397)  评论(0编辑  收藏  举报