Python generator生成器和yield表达式详解


Posted in Python onAugust 08, 2019

前言

Python生成器(generator)并不是一个晦涩难懂的概念。相比于MetaClass和Closure等概念,其较为容易理解和掌握。但相对于程序结构:顺序、循环和分支而言其又不是特别的直观。无论学习任何的东西,概念都是非常重要的。正确树立并掌握一些基础的概念是灵活和合理运用的前提,本文将以一种通俗易懂的方式介绍一下generator和yield表达式。

1. Iterator与Iterable

首先明白两点:

  • Iterator(迭代器)是可迭代对象;
  • 可迭代对象并不一定是Iterator;

比较常见的数据类型list、tuple、dict等都是可迭代的,属于collections.Iterable类型;

迭代器不仅可迭代还可以被内置函数next调用,属于collections.Iterator类型;

迭代器是特殊的可迭代对象,是可迭代对象的一个子集。

将要介绍的gererator(生成器)是types.GeneratorType类型,也是collections.Iterator类型。

也就是说生成器是迭代器,可被next调用,也可迭代。

三者的包含关系:(可迭代(迭代器(生成器)))

  • 迭代器:可用next()函数访问的对象;
  • 生成器:生成器表达式和生成器函数;

2. Python生成器

python有两种类型的生成器:生成器表达式和生成器函数。

由于生成器可迭代并且是iterator,因此可以通过for和next进行遍历。

2.1 生成器表达式

把列表生成式的[]改成()便得到生成器表达式。

>>> gen = (i + i for i in xrange(10))
>>> gen
<generator object <genexpr> at 0x0000000003A2DAB0>
>>> type(gen)
<type 'generator'>
>>> isinstance(gen, types.GeneratorType) and isinstance(gen, collections.Iterator) and isinstance(gen, collections.Iterable)
True
>>>

2.2 生成器函数

python函数定义中有关键字yield,该函数便是一个生成器函数,函数调用返回的是一个generator.

def yield_func():
  for i in xrange(3):
    yield i
gen_func = yield_func()
for yield_val in gen_func:
  print yield_val

生成器函数每次执行到yield便会返回,但与普通函数不同的是yield返回时会保留当前函数的执行状态,再次被调用时可以从中断的地方继续执行。

2.3 next与send

通过for和next可以遍历生成器,而send则可以用于向生成器函数发送消息。

def yield_func():
  for i in xrange(1, 3):
    x = yield i
    print 'yield_func',x
gen_func = yield_func()
print 'iter result: %d' % next(gen_func)
print 'iter result: %d' % gen_func.send(100)

结果:

iter result: 1
yield_func 100
iter result: 2

简单分析一下执行过程:

  • line_no 5 调用生成器函数yield_func得到函数生成器gen_func;
  • line_no 6 使用next调用gen_func,此时才真正的开始执行yield_func定义的代码;
  • line_no 3 执行到yield i,函数yield_func暂停执行并返回当前i的值1.
  • line_no 6 next(gen_func)得到函数yield_func执行到yield i返回的值1,输出结果iter result: 1;
  • line_no 7 执行gen_func.send(100);
  • line_no 3 函数yield_func继续执行,并将调用者send的值100赋值给x;
  • line_no 4 输出调用者send接收到的值;
  • line_no 3 执行到yield i,函数yield_func暂停执行并返回当前i的值2.
  • line_no 7 执行gen_func.send(100)得到函数yield_func运行到yield i返回的值2,输出结果iter result: 2;

如果在上面代码后面再加一行:

print 'iter result: %d' % next(gen_func)

结果:

iter result: 1
yield_func 100
iter result: 2
yield_func None
File "G:\Cnblogs\Alpha Panda\Main.py", line 22, in <module>
  print 'iter result: %d' % next(gen_func)
StopIteration

yield_func只会产生2个yield,但是我们迭代调用了3次,会抛出异常StopIteration。

next和send均会触发生成器函数的执行,使用for遍历生成器函数时不要用send。原因后面解释。

2.4 生成器返回值

使用了yield的函数严格来讲已经不是一个函数,而是一个生成器。因此函数中yield和return是不能同时出现的。

SyntaxError: 'return' with argument inside generator

生成器只能通过yield将每次调用的结果返回给调用者。

2.5 可迭代对象转成迭代器

list、tuple、dict等可迭代但不是迭代器的对象可通过内置函数iter转化为iterator,便可以通过next进行遍历;

这样的好处是可以统一使用next遍历所有的可迭代对象;

tup = (1,2,3)
for ele in tup:
  print ele + ele

上面的代码等价于:

tup_iterator = iter(tup)while True:
  try:
    ele = next(tup_iterator)
  except StopIteration:
    break
  print ele + ele

for循环使用next遍历一个迭代器,混合使用send可能会导致混乱的遍历流程。

其实到这里生成器相关的概念基本已经介绍完成了,自己动手过一遍应该能弄明白了。为了更加深刻的体会生成器,下面我们在往前走一步。

3. range与xrange

在Python 2中这两个比较常用,看一下两者的区别:

  • range为一个内置函数,xrange是一个类;
  • 前者返回一个list,后者返回一个可迭代对象;
  • 后者遍历操作快于前者,且占用更少内存;

这里xrange有点类似于上面介绍的生成器表达式,虽然xrange返回的并不是生成器,但两者均返回并不包含全部结果可迭代对象。

3.1 自定义xrange的Iterator版本

作为一个iterator:

The iterator objects themselves are required to support the following two methods, which together form the iterator protocol:

iterator.__iter__()
Return the iterator object itself. This is required to allow both containers and iterators to be used with the for and in statements. This method corresponds to the tp_iter slot of the type structure for Python objects in the Python/C API.

iterator.next()
Return the next item from the container. If there are no further items, raise the StopIteration exception. This method corresponds to the tp_iternext slot of the type structure for Python objects in the Python/C API.

下面我们自定义class my_xrange:

class my_xrange(object):
  def __init__(self, start, stop = None, step = 1):
    """ 仅仅为了演示,假设start, stop 和 step 均为正整数 """
    self._start = 0 if stop is None else start
    self._stop = start if stop is None else stop
    self._step = step
    self._cur_val = self._start

  def __iter__(self):
    return self
  def next(self):
    if self._start <= self._cur_val < self._stop:
      cur_val = self._cur_val
      self._cur_val += self._step
      return cur_val
    raise StopIteration

测试结果:

import collections
myxrange = my_xrange(0, 10, 3)
res = []
for val in myxrange:
  res.append(val)
print res == range(0, 10, 3) # True
print isinstance(myxrange, collections.Iterator)
# Trueprint isinstance(myxrange, types.GeneratorType)
# False

3.2 使用函数生成器

下面使用函数生成器定义一个generator版的xrange。

def xrange_func(start, stop, step = 1):
  """ 仅仅为了演示,假设start, stop 和 step 均为正整数 """
  cur_val = start
  while start <= cur_val and cur_val < stop:
    yield cur_val
    cur_val += step
isinstance(myxrange, collections.Iterator) and isinstance(myxrange, types.GeneratorType) is True

上面两个自定义xrange版本的例子,均说明生成器以及迭代器保留数列生成过程的状态,每次只计算一个值并返回。这样只要占用很少的内存即可表示一个很大的序列。

4. 应用

不管是迭代器还是生成器,对于有大量有规律的数据产生并需要遍历访问的情景均适用,占用内存少而且遍历的速度快。其中一个较为经典的应用为斐波那契数列(Fibonacci sequence)。

这里以os.walk遍历目录为例来说明yield的应用。如果我们需要遍历一个根目录下的所有文件并根据需要进行增删改查。可能会遇到下列的问题:

预先遍历且缓存结果,但是目录下文件可能很多,而且会动态改变;如果不缓存,多个地方可能会频繁的需要访问这一结果导致效率低下。

这时候可以使用yield定义一个生成器函数。

def get_all_dir_files(target_dir):
  for root, dirs, files in os.walk(target_dir):
    for file in files:
      file_path = os.path.join(root, file)
      yield os.path.realpath(file_path)
def file_factory(file):
  """ do something """
target_dir = './'
all_files = get_all_dir_files(target_dir)
for file in all_files:
  file_factory(file)

限于篇幅,就先介绍到这里,希望本文能让你对生成器有一个新的认识。

,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值

Python 相关文章推荐
解决Python中由于logging模块误用导致的内存泄露
Apr 23 Python
Python实现将不规范的英文名字首字母大写
Nov 15 Python
浅谈python for循环的巧妙运用(迭代、列表生成式)
Sep 26 Python
Python使用requests发送POST请求实例代码
Jan 25 Python
Python贪心算法实例小结
Apr 22 Python
Python 查找字符在字符串中的位置实例
May 02 Python
Python Numpy库datetime类型的处理详解
Jul 13 Python
解决django中ModelForm多表单组合的问题
Jul 18 Python
Python 实用技巧之利用Shell通配符做字符串匹配
Aug 23 Python
python判断链表是否有环的实例代码
Jan 31 Python
Python3实现打印任意宽度的菱形代码
Apr 12 Python
小 200 行 Python 代码制作一个换脸程序
May 12 Python
利用python实现短信和电话提醒功能的例子
Aug 08 #Python
twilio python自动拨打电话,播放自定义mp3音频的方法
Aug 08 #Python
Python 使用matplotlib模块模拟掷骰子
Aug 08 #Python
Pycharm远程调试原理及具体配置详解
Aug 08 #Python
Python IDE Pycharm中的快捷键列表用法
Aug 08 #Python
python多线程与多进程及其区别详解
Aug 08 #Python
python PIL和CV对 图片的读取,显示,裁剪,保存实现方法
Aug 07 #Python
You might like
DC的38部超级英雄动画电影
2020/03/03 欧美动漫
PHP编程中八种常见的文件操作方式
2006/11/19 PHP
PHP mysql与mysqli事务使用说明 分享
2013/08/17 PHP
php绘图之在图片上写中文和英文的方法
2015/01/24 PHP
php解析字符串里所有URL地址的方法
2015/04/03 PHP
php安装扩展mysqli的实现步骤及报错解决办法
2017/09/23 PHP
PHP-X系列教程之内置函数的使用示例
2017/10/16 PHP
php递归函数怎么用才有效
2018/02/24 PHP
PHP设计模式之状态模式定义与用法详解
2018/04/02 PHP
XENON基于JSON变种
2010/07/27 Javascript
Javascript面向对象编程
2012/03/18 Javascript
js实现数组去重、判断数组以及对象中的内容是否相同
2013/11/29 Javascript
WEB前端实现裁剪上传图片功能
2016/10/17 Javascript
jQuery得到多个值只能用取Class ,不能用取ID的方法
2016/12/04 Javascript
移动端脚本框架Hammer.js
2016/12/15 Javascript
微信小程序 刷新上拉下拉不会断详细介绍
2017/05/11 Javascript
Node.js readline 逐行读取、写入文件内容的示例
2018/03/01 Javascript
angularjs下ng-repeat点击元素改变样式的实现方法
2018/09/12 Javascript
JS数组push、unshift、pop、shift方法的实现与使用方法示例
2020/04/29 Javascript
详解element-ui 表单校验 Rules 配置 常用黑科技
2020/07/11 Javascript
Python实现的RSS阅读器实例
2015/07/25 Python
python自动12306抢票软件实现代码
2018/02/24 Python
Pyspider中给爬虫伪造随机请求头的实例
2018/05/07 Python
python爬取足球直播吧五大联赛积分榜
2018/06/13 Python
Numpy之文件存取的示例代码
2018/08/03 Python
python中正则表达式 re.findall 用法
2018/10/23 Python
python hough变换检测直线的实现方法
2019/07/12 Python
查看已安装tensorflow版本的方法示例
2020/04/19 Python
HTML5的download属性详细介绍和使用实例
2014/04/23 HTML / CSS
南威尔士家居商店:Leekes
2016/10/25 全球购物
女方婚礼新郎答谢词
2014/01/11 职场文书
妇女儿童发展规划实施方案
2014/03/16 职场文书
学校志愿者活动总结
2014/06/27 职场文书
个人融资协议书范本两则
2014/10/15 职场文书
mysql 带多个条件的查询方式
2021/06/05 MySQL
html实现弹窗的实例
2021/06/09 HTML / CSS