最近有很多学Python同学问我,Python Generator到底是什么东西,如何理解和使用。Ok,现在就用这篇文章对Python Generator做一个敲骨沥髓的深入解析。
 
为了更好地理解产生器(Generator),还需要掌握另外两个东西:yield和迭代(iterables)。下面就迭代、产生器和yield分别做一个深入的解析。
 
1. 迭代
 
当创建一个列表对象后,可以一个接一个读取列表中的值,这个过程就叫做迭代。
mylist = [1, 2, 3]
for i in mylist:
print(i, end = ' ')
 
mylist对象是可迭代的。在创建列表时,可以使用列表推导表达式,所以从直观上看,列表是可迭代的。
 
mylist = [x*x for x in range(3)]
for i in mylist:
print(i, end=' ') 
 
只要使用for ... in...语句,那么in子句后面的部分一定是一个可迭代的对象,如列表、字典、字符串等。
 
这些可迭代的对象在使用上非常容易理解,我们可以用自己期望的方式读取其中的值。但会带来一个严重的问题。就拿列表为例,如果需要迭代的值非常多,这就意味着需要先将所有的值都放到列表中,而且即使迭代完了列表中所有的值,这些值也不会从内存中消失(至少还会存在一会)。而且如果这些值只需要迭代一次就不再使用,那么这些值在内存中长期存在是没有必要的,所有就产生了产生器(Generator)的概念。
 
2. 产生器(Generator)
 
要理解产生器,首先要清楚产生器到底要解决什么问题,以及产生器的特性。
 
产生器只解决一个问题,就是让需要迭代的值不再常驻内存,也就是解决的内存资源消耗的问题。
为了解决这个问题,产生器也要付出一定的代价,这个代价就是产生器中的值只能访问一次,这也是产生器的特性。
 
下面先看一个最简单的产生器的例子:
# 创建产生器
data_generator = (x*x for x in range(3))
print(data_generator)
for i in data_generator:
print(i, end=' ')
print()
print('第二次迭代data_generator,什么都不会输出')
print()
for i in data_generator:
print(i, end=' ')
 
乍一看这段代码,好像与前面的代码没什么区别。其实,只有一点点区别,就是在创建data_generator对象时使用了一对圆括号,而不是一对方括号。使用一对方括号创建的是列表对象,而使用一对圆括号创建的就是迭代器对象,如果直接输出,会输出迭代器对象的地址,只有通过for...in...语句或调用迭代器的相应方法才能输出迭代器对象中的值。而且第二次对迭代器对象进行迭代,什么都不会输出,这是因为迭代器只能被迭代一次,而且被迭代的值使用完,是不会再保存在内存中的。有点类似熊瞎子摘苞米,摘一穗,丢一穗。
 

执行这段代码,会输出如下结果:

<generator object <genexpr> at 0x7f95e0154150>
0 1 4
第二次迭代data_generator,什么都不会输出
 
3. yield
 
到现在为止,我们已经对产生器要解决的问题,以及特性有了一个基本了解,那么产生器是如何做到这一点的呢?这就要依靠yield语句了。
 
现在让我们先来看一个使用yield的例子。

# 编写产生器函数
def generate_even(max):
for i in range(0, max + 1):
if i % 2 == 0:
yield i
print(generate_even(10))
even_generator = generate_even(10)
for n in even_generator:
print(n, end=' ')
 
这段代码的目的是输出不大于10的所有偶数,其中generator_even是一个产生器函数。我们注意到,在该函数中每找到一个偶数,就会通过yield语句指定这个偶数。那么这个yield起什么作用呢?
 
再看看后面的代码,首先调用generator_even函数,并将返回值赋给even_generator变量,这个变量的类型其实是一个产生器对象。而for...in...循环中的in子句后面则是这个产生器对象,而n则是产生器中的每一个值(偶数)。执行这段代码,会输出如下结果:

<generator object generate_even at 0x7f814826c450>
0 2 4 6 8 10
 
现在先谈谈执行yield语句会起到什么效果。其实yield语句与return语句一样,都起到返回的作用。但yield与return不同,如果执行return语句,会直接返回return后面表达式的值。但执行yield语句,返回的是一个产生器对象,而且这个产生器对象的当前值就是yield语句后面跟着的表达式的值。调用yield语句后,当前函数就会返回一个迭代器,而且函数会暂停执行,直到对该函数进行下一次迭代。
 
可能读到这些解释,有的读者还是不太明白,什么时候进行下一次迭代呢?如果不使用for...in...语句,是否可以对产生器进行迭代呢?其实迭代器有一个特殊方法__next__。每次对迭代器的迭代,本质上都是在调用__next__方法。
 
那么还有最后一个问题,for...in...语句在什么时候才会停止迭代呢?其实for...in...语句在底层会不断调用in子句后面的可迭代对象的__next__方法,直到该方法抛出StopIteration异常为止。也就是说,可以将上面的for...in...循环改成下面的代码。连续调用6次__next__方法,返回0到10,一共6个偶数,当第7次调用__next__方法时,产生器中已经没有值了,所以会抛出StopIteration异常。由于for...in...语句自动处理了StopIteration异常,所以循环就会自动停止,但当直接调用__next__方法时,如果产生器中没有值了,就会直接抛出StopIteration异常,除非使用try...except...语句捕获该异常,否则程序会异常中断。
even_generator = generate_even(10)
print(even_generator.__next__())
print(even_generator.__next__())
print(even_generator.__next__())
print(even_generator.__next__())
print(even_generator.__next__())
print(even_generator.__next__())
# print(even_generator.__next__()) # 抛出StopIteration异常

总结:产生器本质上就是动态产生待迭代的值,使用完就直接扔掉了,这样非常节省内存空间,但这些值只能被迭代一次。

 
4. 用普通函数模拟产生器函数的效果
 
如果你看到一个函数中使用了yield语句,说明该函数是一个产生器。其实可以按下面的步骤将该产生器函数改造成普通函数。
 
1. 在函数的开始部分定义一个列表变量,代码如下:

result = []
2. 将所有的yield expr语句都替换成下面的语句:
result.append(expr)
3. 函数的最后执行return result返回这个列表对象
 
 
为了更清晰表明这个转换过程,现在给出一个实际的案例:
 
# 产生不大于max的偶数
def generate_even(max):
for i in range(0, max + 1):
if i % 2 == 0:
yield i even_generator = generate_even(10)
for n in even_generator:
print(n, end=' ') # 将产生器函数改造成普通函数,实际上,就是将yield后面表达式的值都添加在列表中
def generate_even1(max):
evens = []
for i in range(0, max + 1):
if i % 2 == 0:
evens.append(i)
return evens
print()
even_list = generate_even1(10)
for n in even_list:
print(n, end=' ')

在这段代码中有两个函数:generate_even和generate_even1,其中generate_even是产生器函数,generate_even1是普通函数(与generate_even函数的功能完全相同)。按着前面的步骤,将所有产生的偶数都添加到了列表变量evens中,最后返回这个列表变量。这两个函数在使用方式上完全相同。不过从本质上说,generate_even函数是动态生成的偶数,用完了就扔,而generate_even1函数事先将所有产生的偶数都添加到列表中,最后返回。所以从generate_even1函数的改造过程来看,yield的作用就相当于使用append方法将表达式的值添加到列表中,只不过yield并不会保存表达式的值,而append方法会保存表达式的值。

 
 5.与迭代相关的API
 
这一节来看一看Python为我们提供了哪些与迭代相关的API
 
Python SDK提供了一个itertools模块,该模块中的API都与迭代相关,例如,可以通过chain.from_iterable方法合并多个可迭代对象,通过permutations函数以可迭代对象形式返回列表的全排列。
from itertools import *

# 这里每一个yield的值必须是可迭代的,才能用chain.from_iterable方法合并
def make_iterables_to_chain():
yield [1,2,3]
yield ['a','b','c']
yield ['hello','world'] for v in make_iterables_to_chain():
print(v)
# 将所有可迭代对象合并成一个可迭代对象
for v in chain.from_iterable(make_iterables_to_chain()):
print('<',v,'>', end = ' ')
print('-------上面的代码相当于下面的写法-------')
a = [1,2,3]
a.extend(['a','b','c'])
a.extend(['hello','world'])
print(a)
for v in a:
print('[',v,']', end = ' ')
# 以可迭代对象形式返回列表的全排列
values = [1,2,3,4]
values_permutations = permutations(values)
for p in values_permutations:
print(p)
执行这段代码,会输出如下内容:
[1, 2, 3]
['a', 'b', 'c']
['hello', 'world']
< 1 > < 2 > < 3 > < a > < b > < c > < hello > < world > -------上面的代码相当于下面的写法-------
[1, 2, 3, 'a', 'b', 'c', 'hello', 'world']
[ 1 ] [ 2 ] [ 3 ] [ a ] [ b ] [ c ] [ hello ] [ world ] (1, 2, 3, 4)
(1, 2, 4, 3)
(1, 3, 2, 4)
(1, 3, 4, 2)
(1, 4, 2, 3)
(1, 4, 3, 2)
(2, 1, 3, 4)
(2, 1, 4, 3)
(2, 3, 1, 4)
(2, 3, 4, 1)
(2, 4, 1, 3)
(2, 4, 3, 1)
(3, 1, 2, 4)
(3, 1, 4, 2)
(3, 2, 1, 4)
(3, 2, 4, 1)
(3, 4, 1, 2)
(3, 4, 2, 1)
(4, 1, 2, 3)
(4, 1, 3, 2)
(4, 2, 1, 3)
(4, 2, 3, 1)
(4, 3, 1, 2)
(4, 3, 2, 1)
 更多免费视频课程,请识别下面二维码看我的B站

对本文感兴趣,可以加李宁老师微信公众号(unitymarvel)

关注  「极客起源」  公众号,获得更多免费技术视频和文章。

连Python产生器(Generator)的原理都解释不了,还敢说Python用了5年?的更多相关文章

  1. python 装饰器、递归原理、模块导入方式

    1.装饰器原理 def f1(arg): print '验证' arg() def func(): print ' #.将被调用函数封装到另外一个函数 func = f1(func) #.对原函数重新 ...

  2. Python装饰器详解

    python中的装饰器是一个用得非常多的东西,我们可以把一些特定的方法.通用的方法写成一个个装饰器,这就为调用这些方法提供一个非常大的便利,如此提高我们代码的可读性以及简洁性,以及可扩展性. 在学习p ...

  3. http://python.jobbole.com/85056/ 简单 12 步理解 Python 装饰器,https://www.cnblogs.com/deeper/p/7482958.html另一篇文章

    好吧,我标题党了.作为 Python 教师,我发现理解装饰器是学生们从接触后就一直纠结的问题.那是因为装饰器确实难以理解!想弄明白装饰器,需要理解一些函数式编程概念,并且要对Python中函数定义和函 ...

  4. Python装饰器与闭包

    闭包是Python装饰器的基础.要理解闭包,先要了解Python中的变量作用域规则. 变量作用域规则 首先,在函数中是能访问全局变量的: >>> a = 'global var' & ...

  5. Python 装饰器学习

    Python装饰器学习(九步入门)   这是在Python学习小组上介绍的内容,现学现卖.多练习是好的学习方式. 第一步:最简单的函数,准备附加额外功能 1 2 3 4 5 6 7 8 # -*- c ...

  6. (转载)Python装饰器学习

    转载出处:http://www.cnblogs.com/rhcad/archive/2011/12/21/2295507.html 这是在Python学习小组上介绍的内容,现学现卖.多练习是好的学习方 ...

  7. Python装饰器学习

    Python装饰器学习(九步入门)   这是在Python学习小组上介绍的内容,现学现卖.多练习是好的学习方式. 第一步:最简单的函数,准备附加额外功能 ? 1 2 3 4 5 6 7 8 # -*- ...

  8. Python基础(五) python装饰器使用

    这是在Python学习小组上介绍的内容,现学现卖.多练习是好的学习方式. 第一步:最简单的函数,准备附加额外功能 # -*- coding:gbk -*- '''示例1: 最简单的函数,表示调用了两次 ...

  9. (二)Python 装饰器

    1. 函数 在 Python 中,使用关键字 def 和一个函数名以及一个可选的参数列表来定义函数.函数使用 return 关键字来返回值.定义和使用一个最简单的函数例子: >>> ...

随机推荐

  1. HDU - 6570 - Wave(暴力)

    Avin is studying series. A series is called "wave" if the following conditions are satisfi ...

  2. CSS中的包含块

    1.初始包含块,浏览器viewport大小 2.非根元素,position:relative/static,包含块为最近的块级框,表格单元或行内祖先框的内容区 3.非根元素,position:abso ...

  3. CentOS 7常用命令

    常用命令 文件与目录操作 命令 解析 cd /home 进入 ‘/home’ 目录 cd .. 返回上一级目录 cd ../.. 返回上两级目录 cd - 返回上次所在目录 cp file1 file ...

  4. 理解C#回调函数

    序言 本篇主要学习了C#回调函数的定义使用.欢迎各位大牛的指导. 正文 回调函数是什么? 回调函数就是一个通过函数指针调用的函数.如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调 ...

  5. Java-Collection和Map

    创建博客的目的主要帮助自己记忆和复习日常学到和用到的知识:或有纰漏请大家斧正,非常感谢! 之前面试,被问过一个问题:List和Set的区别. 主要区别很明显了,两者都是数组形式存在的,继承了Colle ...

  6. C++ Templates (2.3 类模板的局部使用 Partial Usage of Class Templates)

    返回完整目录 目录 2.3 类模板的局部使用 Partial Usage of Class Templates 2.3.1 Concepts 2.3 类模板的局部使用 Partial Usage of ...

  7. python代码开发规范

    https://github.com/libo-sober/LearnPython/tree/master/day18 为什么要有模块? 拿来主义,提高开发效率. 便于管理维护. 什么是脚本呢? 脚本 ...

  8. Oracle闪回flashback

    参考资料:Using Oracle Flashback Technology Oracle 11g的新特性闪回操作 闪回查询 闪回查询 闪回版本查询 闪回事务查询 闪回数据 闪回表 闪回删除 闪回数据 ...

  9. Redis—HyperLogLog

    HyperLogLog 实现一个功能 统计网站的UV (user view),区别PV (page view) 数据去重 统计总数 同一个用户的反复点击进入记为 1 次 解决方案 最简单的思路是记录集 ...

  10. Docker:常见命令

    Docker常见命令(持续更新中):   说明 命令 截图 1 查看Docker版本 Docker -v 2  查看镜像 Docker image ls   3  查看容器  Docker conta ...