Python学习笔记16:生成器
在前文Python学习笔记15:推导式的结尾我们已经引出了Python中的另一项特性:生成器。
要说明的是生成器和推导式在写法上是极为相似的,除了生成器是用()
来包裹以外。但实际上,他们有很大不同。
基本概念
我们现在看一个简单的例子:
import requests
urls = ("https://www.liaoxuefeng.com/wiki/1016959663602400/1183249464292448",
"https://www.runoob.com/w3cnote/python-spider-intro.html",
"https://www.runoob.com/w3cnote/secure-wordpress-nginx.html",
"https://cn.python-requests.org/zh_CN/latest/")
resps = []
for url in urls:
resp = requests.get(url)
resps.append(resp)
for resp in resps:
print(len(resp.text))
在这个例子中,我们读取一个url列表,然后用requests
模块获取其内容,然后输出内容的长度。
我们现在改写为推导式:
import requests
urls = ("https://www.liaoxuefeng.com/wiki/1016959663602400/1183249464292448",
"https://www.runoob.com/w3cnote/python-spider-intro.html",
"https://www.runoob.com/w3cnote/secure-wordpress-nginx.html",
"https://cn.python-requests.org/zh_CN/latest/")
resps = [requests.get(url) for url in urls]
for resp in resps:
print(len(resp.text))
如果你注意输出的话,就不难发现,程序需要等待一会,然后才会一股脑输出所有结果。
这也不难理解,因为输出是要等到推导式处理完所有url后才会开始,那如果我们把推导式换成生成器呢?
import requests
urls = ("https://www.liaoxuefeng.com/wiki/1016959663602400/1183249464292448",
"https://www.runoob.com/w3cnote/python-spider-intro.html",
"https://www.runoob.com/w3cnote/secure-wordpress-nginx.html",
"https://cn.python-requests.org/zh_CN/latest/")
for resp in (requests.get(url) for url in urls):
print(len(resp.text))
注意,生成器必须放在一个循环语句中,所以这里并没有采用复制给
resps
变量的做法。
我们可以注意到,此时结果输出是依次进行,中间会有或长或短的间隔,这是因为生成器的特性与推导式大为不同。
优缺点
如果你接触过安卓开发,可能会知道安卓开发中的gallery
组件,也就是我们日常使用手机中很常见的预览图片时候左右滑动的那个,这个组件的实现其实和Python中的生成器异曲同工。
要知道,在很多时候我们的程序依然要考虑硬件性能,比如加载巨量数据或者图片等很占用资源的内容。而安卓平台的gallery
组件就是如此,因为图片加载是很浪费内存的,你不可能把一组图片全部都加载入内存,一来浪费内存,二来也没有必要,毕竟用户一次只能看到一张图片。所以安卓给gallery
加入了这样的特性:一次只会把当前图片和前后两张图片加载入内存,这样用户在左右滑动的时候几乎不会感觉到加载延迟,但又极大节省了不必要的资源消耗。
而Python的生成器和gallery
的理念完全一致,可以把它看成一个Python版的gallery
,当我们用循环或别的方式获取这个生成器的某一个元素时,生成器才会实时生成这个元素,当你不再访问这个元素的时候,它会立即抛弃。
所以在上边这个例子中,我们是一边获取url
一边读取网络内容,然后进入以此往复。
我们可以看到生成器具有以下优点:
- 节约系统资源。
- 可以将集中性的耗时操作切分为小段,避免长时间没有响应。
但显然也不是没有缺点的,如果我们需要一次性获取多个元素并依照这多个元素来进行处理,就显得很不方便了。
生成器函数
除了直接把生成器作为表达式使用,我们还可以将其包装成函数。
import requests
urls = ("https://www.liaoxuefeng.com/wiki/1016959663602400/1183249464292448",
"https://www.runoob.com/w3cnote/python-spider-intro.html",
"https://www.runoob.com/w3cnote/secure-wordpress-nginx.html",
"https://cn.python-requests.org/zh_CN/latest/")
def getUrlRes(urls: tuple):
for url in urls:
yield requests.get(url)
for resp in getUrlRes(urls):
print(len(resp.text))
在上边这个例子中,我们把生成器包装成了一个函数getUrlRes
,这很有用,我们屏蔽了生成器的实现细节,其它人只要使用就可以了。
应该注意到,我们在生成器里返回值的时候使用的是yield
而非return
。这不难理解,如果使用return
,程序会立即返回,getUrlRes
也不会再次执行,而yield
将告诉解释器,这里是一个生成器,在外部程序获取一个值的同时,生成器函数会挂起,直到外部程序进入下一次循环/遍历,此时生成器会从挂起的地方再次运行,并返回下一个值。
好了,关于生成器的话题就到这里了,接下来的一段时间我会探究一下Python中面向对象的部分,比照其它语言,挖掘一下前边没有深入的部分。在这部分梳理完毕后,我会重新阅读一遍《Head First 设计模式》,并用Python和类图的形式整理一遍设计模式的内容。
谢谢阅读。