Python语言高级特性:生成器 #P007#

在Python这门语言中,生成器毫无疑问是最有用的特性之一,与此同时,也是使用的最不广泛的Python特性之一。究其原因,主要是因为,在其他主流语言里面没有生成器的概念。正是由于生成器是一个“新”的东西,所以,它一方面没有引起广大工程师的重视,另一方面,也增加了工程师的学习成本,最终导致大家错过了Python中如此有用的一个特性。在这一节中,我们将深入浅出地介绍Python的生成器,以改变“如此有用的特性却使用极不广泛”的现象。

1 迭代器协议

生成器自动实现了迭代器协议,而迭代器协议对很多人来说,也是一个较为抽象的概念。所以,为了更好的理解生成器,需要简单的了解一下迭代器协议的概念。迭代器协议是指: 对象需要提供next方法,它要么返回迭代中的下一项,要么就引起一个StopIteration异常以终止迭代。可迭代对象就是实现了迭代器协议的对象。所谓协议,只是一种约定,可迭代对象实现迭代器协议,Python的内置工具(如for循环、sum、min、max函数等)使用迭代器协议访问对象。

例如,在所有语言中,我们都可以使用for循环来遍历数组。Python的list底层实现是一个保存对象引用的数组。因此,我们可以使用for循环来遍历list。如下所示:

In [1]: for n in [1, 2, 3, 4]:
   ...:     print(n)
   ...:     
1
2
3
4

但是,在Python语言中,我们不但可以用for循环来遍历list,也可以用来遍历文件对象。如下所示:

In [2]: with open('/etc/passwd') as f:
   ...:     for line in f:
   ...:         print(line)

为什么在Python中,文件还可以使用for循环进行遍历呢?这是因为,在Python语言中,文件对象实现了迭代器协议。for循环并不知道它遍历的是一个文件对象,它只管使用迭代器协议访问对象。正是由于Python的文件对象实现了迭代器协议,我们才得以使用如此方便的方式访问文件。如下所示:

In [3]: f = open('/etc/passwd')

In [4]: dir(f)
Out[4]: 
['__class__',
 '__delattr__',
 '__doc__',
  ......
 'next',
 'readline',
 'readlines']

我们通过Python内置的dir函数可以看到,文件对象具有一个名为next的方法。next方法就是迭代器协议的一部分,它要么返回迭代中的下一项,要么就抛出一个StopIteration异常。for循环会自动调用next方法获取文件中的内容,与此同时,for循环也会通过结束循环的方式,实现自动处理StopIteration异常。

2 生成器

理解了迭代器协议和可迭代对象以后,再来看生成器就会好理解很多。

Python使用生成器对延迟操作提供了支持。所谓延迟操作,是指在需要的时候才产生结果,而不是立即产生结果。Python有两种不同的方式提供生成器:

  1. 生成器函数:与普通函数定义类似,但是,使用yield语句而不是return语句返回结果。yield语句一次返回一个结果,在每个结果中间,挂起函数的状态,以便下次从它离开的地方继续执行;
  2. 生成器表达式:类似于列表推导,但是,生成器返回按需产生结果的一个对象,而不是一次构建一个结果列表。

下面是一个生成器的例子,使用生成器返回自然数的平方:

def gensquares(N):
    for i in range(N):
        yield i ** 2

for item in gensquares(5):
    print(item)

相同的功能,使用普通函数实现:

def gensquares(N):
    res = []
    for i in range(N):
        res.append(i*i)
    return res

for item in gensquares(5):
    print(item)

读者可以看到,使用生成器以后,实现相同功能,代码行数更少了。在我们的gensquares函数中,原来需要5行代码,现在只需要3行代码即可。

我们再来看一个例子,以加深读者对生成器的理解。假设现在有一个列表,列表中包含若干整数。接下来,我们需要对列表进行过滤。在过滤以后的结果中,只保留列表中偶数的数字。这个需求是如此的简单,相信读者能够快速的写出相应的函数。如下所示:

def get_even_num(l):
    res = []
    for item in l:
        if item % 2 == 0:
            res.append(item)
    return res

def main():
    l = range(5)
    for item in get_even_num(l):
        print(item)

if __name__ == '__main__':
    main()

使用生成器以后,程序整体架构不变,但是代码更加清晰。如下所示:

def get_even_num(l):
    for item in l:
        if item % 2 == 0:
            yield item

def main():
    l = range(5)
    for item in get_even_num(l):
        print(item)

if __name__ == '__main__':
    main()

接下来看一下生成器表达式的使用。生成器表达式与列表推导非常相似,使用列表推导,将会一次产生所有结果。使用生成器,不会一次产生所有的结果,它会返回按需产生结果的一个对象。如下所示:

In [5]: squares = [x**2 for x in range(5)]

In [6]: squares
Out[6]: [0, 1, 4, 9, 16]

将列表推导的中括号,替换成圆括号,就是一个生成器表达式:

In [7]: squares = (x**2 for x in range(5))

In [8]: squares
Out[8]: <generator object <genexpr> at 0x7fe8333bf280>

In [9]: next(squares)
Out[9]: 0

In [10]: next(squares)
Out[10]: 1

In [11]: next(squares)
Out[11]: 4

In [12]: list(squares)
Out[12]: [9, 16]

在Python语言中,不但使用迭代器协议让for循环变得更加通用,而且,大部分内置函数也可以使用迭代器协议访问对象。例如, sum函数是Python的内置函数,该函数使用迭代器协议访问对象,而生成器实现了迭代器协议,所以,我们可以直接这样计算一系列值的和:

In [13]: sum((x ** 2 for x in range(4))) 
Out[13]: 14

为了简单起见,我们也可以省略生成器的圆括号。如下所示:

In [14]: sum(x ** 2 for x in range(4)) 
Out[14]: 14

使用sum函数计算一些列值的和时,可以直接使用生成器,而不用多此一举的先构造一个列表。例如,下面的程序就是一个典型的反面教材。在这段程序中,需要先构造一个列表,然后再使用sum函数计算列表中元素的和:

In [15]: sum([x ** 2 for x in range(4)])
Out[15]: 14

3 生成器与函数

通过前面的介绍,相信读者已经对Python的生成器有了感性的认识。接下来,以生成器函数为例,再来深入探讨一下Python的生成器。

Python的生成器语法上和函数类似。生成器函数和常规函数几乎是一样的,它们都是使用def语句进行定义。差别在于:

  1. 从语法上看,生成器使用yield语句返回一个值,而常规函数使用return语句返回一个值;
  2. 从使用上看,Python的生成器自动实现了迭代器协议。由于生成器实现了迭代器协议,所以,我们可以在迭代环境中(如for循环、sum函数、min函数等)使用生成器;
  3. 从Python实现上看,生成器的yield语句挂起生成器函数的状态,保留足够的信息,以便之后从它离开的地方继续执行。

使用生成器的主要原因是,生成器支持延迟计算。所谓延迟计算,就是一次返回一个结果,而不是一次返回所有结果。这对于大数据量处理,将会非常有用。读者可以在自己计算机上试试下面两个表达式,并且观察内存占用情况。对于前一个表达式,笔者在自己的电脑上进行测试,还没有看到最终结果电脑就已经卡死。对于后一个表达式,几乎没有什么内存占用。

In [16]: sum([i for i in xrange(1000000000)])
In [17]: sum(i for i in xrange(1000000000))

除了延迟计算,生成器还能有效提高代码可读性。例如,现在有一个需求,求一段文字中,每个单词出现的位置。如果我们不使用生成器,我们的程序大概是下面这样:

def index_words(text):
    result = []
    if text:
        result.append(0)
    for index, letter in enumerate(text, 1):
        if letter == ' ':
            result.append(index)
    return result

text = """The Zen of Python, by Tim Peters"""
print(index_words(text))

使用生成器以后,程序变得更加清晰了:

def index_words(text):
    if text:
        yield 0
    for index, letter in enumerate(text, 1):
        if letter == ' ':
            yield index

text = """The Zen of Python, by Tim Peters"""
print(list(index_words(text)))

至少有两个理由可以说明 ,使用生成器比不使用生成器代码更加清晰:

  1. 使用生成器以后,代码行数更少。读者需要牢记记住,如果想把代码写得简洁优美,需要在保证代码可读性的前提下,代码行数越少越好;
  2. 不使用生成器的时候,对于每次结果,我们首先看到的是一个列表的append操作(result.append(index)),其次,我们才看到的是index这个变量。也就是说,我们每次看到的是一个列表的append操作,只是append的参数是我们想要的结果。使用生成器以后,直接yield index,少了列表append操作的干扰,一眼就能够看出,代码是要返回index这个变量的值。

这个例子充分说明了,合理使用生成器,能够有效提高代码可读性。只要大家接受了生成器的概念,理解了yield语句和return语句一样,也是返回一个值。那么,就能够理解生成器的好处,也可以通过生成器提高程序效率和可读性。

4 总结

生成器本身是一个可选的特性,所有使用生成器的地方,都可以用一个不使用生成器的函数来代替。之所以需要使用生成器,一方面是为了对大数据量的计算提供支持,另一方面,是为了提高代码的可读性。在这篇文章中,我们反复强调,要想把代码写得简洁优美,需要在保证可读性的前提下,代码行数越少越好。生成器就是一个改善代码质量的例子,似乎可有可无,但是,很多问题使用生成器都可以解决得更加优美。

作者介绍

赖明星,架构师、作家。现就职于腾讯,参与并主导下一代金融级数据库平台研发。有多年的 Python 开发经验和一线互联网实战经验,擅长 C、Python、Java、MySQL、Linux 等主流技术。国内知名的 Python 技术专家和 Python 技术的积极推广者,著有《Python Linux 系统管理与自动化运维》一书。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1. 根据要求实现资源下载器。 - 启动后,让用户选择专区,每个专区用单独的函数实现,提供的专区如下: - 下载 花瓣网图片专区 - 下载 抖音短视频专区 - 下载 NBA锦集 专区 - 在用户选择了某个功能之后,表示进入某下载专区,在里面循环提示用户可以下载的内容选项(已下载过的则不再提示下载) 提醒:可基于全部变量保存已下载过得资源。 - 在某个专区中,如果用户输入(Q/q)表示 退出上一级,即:选择专区。 - 在选择专区如果输入Q/q则退出整个程序。 - 每个专区实现下载的案例如下: - 图片 ```python # 可供用户下载的图片如下 image_dict = { "1":("吉他男神","https://hbimg.huabanimg.com/51d46dc32abe7ac7f83b94c67bb88cacc46869954f478-aP4Q3V"), "2":("漫画美女","https://hbimg.huabanimg.com/703fdb063bdc37b11033ef794f9b3a7adfa01fd21a6d1-wTFbnO"), "3":("游戏地图","https://hbimg.huabanimg.com/b438d8c61ed2abf50ca94e00f257ca7a223e3b364b471-xrzoQd"), "4":("alex媳妇","https://hbimg.huabanimg.com/4edba1ed6a71797f52355aa1de5af961b85bf824cb71-px1nZz"), } ``` ```python # 下载图片示例 import request res = requests.get( url="https://hbimg.huabanimg.com/4edba1ed6a71797f52355aa1de5af961b85bf824cb71-px1nZz", headers={ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36" } ) with open("alex媳妇.png",mode="wb") as f: f.write(res.content) ``` - 短视频 ```python # 可供用户下载的短视频如下 video_dict = { "1":{"title":"东北F4模仿秀",'url':"https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0300f570000bvbmace0gvch7lo53oog"}, "2":{"title":"卡特扣篮",'url':"https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f3e0000bv52fpn5t6p007e34q1g"}, "3":{"title":"罗斯mvp",'url':"https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f240000buuer5aa4tij4gv6ajqg"}, } ``` ```python # 下载视频示例 import requests res = requests.get( url="https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f240000buuer5aa4tij4gv6ajqg", headers={ "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 FS" } ) with open('罗斯mvp.mp4', mode='wb') as f: f.write(res.content) ``` - NBA ```python # 可供用户下载的NBA视频如下 nba_dict = { "1":{"title":"威少奇才首秀三双","url":"https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0300fc20000bvi413nedtlt5abaa8tg&ratio=720p&line=0"}, "2":{"title":"塔图姆三分准绝杀","url":"https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0d00fb60000bvi0ba63vni5gqts0uag&ratio=720p&line=0"} } ``` ```python # 下载示例 import requests res = requests.get( url="https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0d00fb60000bvi0ba63vni5gqts0uag&ratio=720p&line=0", headers={ "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 FS" } ) with open('塔图姆三分准绝杀.mp4', mode='wb') as f: f.write(res.content) ```
03-25
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值