Python3 生成器(generator)概念浅析

news/2024/7/19 12:34:43 标签: python, 程序人生, 爬虫, 数据分析, 人工智能

<a class=python-generator.png" />

引子

某次面试问候选人:Python 中生成器是什么?答曰:有 yield 关键字的函数。而在我印象中此种函数返回的值是生成器,而函数本身不是。如下:

In [1]: def get_nums(n): 
   ...:     for i in range(n): 
   ...:         yield i 
   ...:                                                                                                                                                                  
In [2]: type(get_nums)
Out[2]: function
  
In [3]: nums = get_nums(10)   
  
In [4]: type(nums)
Out[4]: generator

但看候选人那么笃定,隐隐然感觉哪里不对,于是有了以下探究。

概念

要弄清楚 Python3 中这些概念的区别,最权威的当然是去看官方的术语对照表:

generator

​ A function which returns a generator iterator. It looks like a normal function except that it contains yield expressions for producing a series of values usable in a for-loop or that can be retrieved one at a time with the next() function.

​ Usually refers to a generator function, but may refer to a generator iterator in some contexts. In cases where the intended meaning isn’t clear, using the full terms avoids ambiguity.

generator iterator

​ An object created by a generator function.

​ Each yield temporarily suspends processing, remembering the location execution state (including local variables and pending try-statements). When the generator iterator resumes, it picks up where it left off (in contrast to functions which start fresh on every invocation).

generator expression

​ An expression that returns an iterator. It looks like a normal expression followed by a for clause defining a loop variable, range, and an optional if clause.

可以看出,Python generator 相关的概念主要有三个,分别是 generatorgenerator expressiongenerator iterator。上面也特别指出了, genrator 在有的上下文中指的是 generator function(如候选人所说),但在另外一些上下文中却指的是 generator iterator(如上面例子解释器告诉我们的)。为了避免歧义,大家在表达时可以尽量说全称。

结合我的一些经验,可以将其归纳为以下三个概念:

  • Generator Function:含有 yield 关键字的函数,会返回一系列值,可以使用 next() 对其返回值进行迭代。
  • Generator Iterator:generator function 返回的对象。可以进行一次性地迭代。
  • Generator Expression:可以求值为 generator iterator 的表达式。使用小括号和 for 来定义,如下面例子:
In [5]: a = (i*i for i in range(10))                                                                                                                                                                                                                                     

In [6]: type(a)                                                                                                                                                                                                                                                          
Out[6]: generator

深入

生成器 vs 迭代器

从中文字面可能不好理解它们的关系,但是从上文提到的英文术语来分析:generator iterator。他们的关系就一目了然了:

迭代器(Iterator)是一种更宽泛的概念,生成器(generator Iterator)是一种迭代器,但反过来不成立。

迭代器是任何实现了 __next__ 方法的对象(object),可以通过 next(iterator) 对其进行迭代,迭代结束时会抛出 StopIteration 异常。

while True:
  try:
    x = next(an_iterator)
    do_sth_with(x)
  except StopIteration:
    break
 

通常我们会使用 for in 来对其进行简化迭代:

for x in an_iterator:
  do_sth_with(x)

需要python全套入门学习资料的小伙伴私信领取即可

yield 原理

yield 是一个神奇的关键字,它会临时挂起当前函数,记下其上下文(包括局部变量、待决的 try catch 等),将控制权返回给函数调用者。当下一次再调用其所在 generator function 时,会恢复保存的上下文,继续执行剩下的语句,直到再遇到 yield 或者退出为止。

我们常见的 return 是与之相对的关键字,但 return 会结束函数调用,销毁上下文(弹出栈帧),将控制权返回给调用者。

因此,以 yield 进行执行流控制的函数称为 generator function,以 return 进行执行流控制的函数,就是普通的 function 喽~

当然,由于可以临时挂起函数的执行,yield 还有更高阶的用法,即充当其调用者和被挂起函数间交互的桥梁:

In [1]: def dynamic_step_seq(size, start=0, default_step=1): 
    ...:   x = start 
    ...:   for _ in range(size): 
    ...:     given_step = yield x 
    ...:     if given_step is not None: 
    ...:       x += given_step 
    ...:     else: 
    ...:       x += default_step 
    ...:                                                                                                                                                                                                                                                                 

In [2]: for x in dynamic_step_seq(10): 
    ...:     print(x, end=' ') 
    ...: print()                                                                                                                                                                                                                                                         
0 1 2 3 4 5 6 7 8 9 
In [3]: dss = dynamic_step_seq(10)                                                                                                                                                                                                                                      

In [4]: next(dss) # 注                                                                                                                                                                                                                                                
Out[5]: 0

In [6]: dynamic_step = 1 
    ...: while True: 
    ...:     try: 
    ...:         x = dss.send(dynamic_step) 
    ...:         dynamic_step += 1 
    ...:         print(x, end=' ') 
    ...:     except StopIteration: 
    ...:         print() 
    ...:         break 
    ...:                                                                                                                                                                                                                                                                 
1 3 6 10 15 21 28 36 45 
  

:此处初看有些奇怪,但是通过 yield 作用我们能推断出原理:需要首先调用 next 将函数运行至 yield 处,才能通过 generator.send 给 generator 传送对象。

效用

那么使用生成器有什么好处呢?简单来说,主要有两大好处:

  • 精简代码
  • 提高性能

精简代码

使用 yield 关键字或者生成器表达式可以很方便的生成一个迭代器对象。为了说明这一点,我们来比较一下对于一个需求的不同实现。该需求很简单:获取前 n 个自然数。

最直观的方法为,构造一个数组然后返回:

# Build and return a list
def firstn(n):
    num, nums = 0, []
    while num < n:
        nums.append(num)
        num += 1
    return nums

sum_of_first_n = sum(firstn(1000000))

当 n 很小的时候,该实现没有什么问题,但是当 n 变得很大,你的机器内存是吃不消的:

In [4]: a = firstn(10000000000000000000)                                                                                                                                                                                                                                 
Killed

IPython 直接内存爆掉被 kill 了。

于是,我们很自然的想起可以用生成器模式,但是你仍然不想用 yield,于是你需要构造一个对象,并实现 __iter____next__ 方法:

# Using the generator pattern (an iterable)
class firstn(object):
    def __init__(self, n):
        self.n = n
        self.num, self.nums = 0, []

    def __iter__(self):
        return self

    def __next__(self):
        if self.num < self.n:
            cur, self.num = self.num, self.num+1
            return cur
        else:
            raise StopIteration()

sum_of_first_n = sum(firstn(1000000))

:在 Python3 中,如果你的类实现了__iter__ ,则成为一个可迭代对象,可以调用 iter(instance) 得到一个迭代器。如果你的类实现了 __next__,那么你的类实例本身就成为了一个迭代器,可以通过 next(instance) 来调用,进行迭代。

需要python全套入门学习资料的小伙伴私信领取即可

这些代码终于实现了我们的要求。但是,它有如下问题:

  1. 为了实现一个简单的需求却不得不构造冗长的代码。
  2. 使用了很多语言约定,读起来不直观。
  3. 逻辑表达的绕来绕去。

于是,你忍不住了,终于使用了 yield:

# a generator that yields items instead of returning a list
def firstn(n):
    num = 0
    while num < n:
        yield num
        num += 1

sum_of_first_n = sum(firstn(1000000))

于是你使用了寥寥几行,构造出了一个生成器函数。

其实[坏笑],Python 3 中,就有该函数: range(n)

利用此函数以及 Iterator expression,你可以用很紧凑的代码构造出很多迭代数列

squares = (x * x for x in range(n))
evens = (2 * x for x in range(n))

注意,他们都只能迭代一次。

提高性能

这一条主要是针对内存使用上来说的。因为迭代器不会保存所有值,而是在运行中动态的计算出数列的各个值,并将之前的数值扔掉,因此是很省内存的,这点在前面的例子也有体现。这里举另外一个更实际一点例子。

假设我们有一个很大的文件(比如说 8G) ,但是你的电脑只有 4G 内存,你如何利用 Python 对其进行处理?

答案是使用 yield 构造 generator Iterator:

def read_by_chunks(file, chunk_size=1024):
    while True:
        data = file.read(chunk_size)
        if not data:
            break
        yield data


f = open('your_big_file.dat')
for chunk in read_by_chunks(f):
    process_chunk(chunk)

这种通过构造 generator 逐块读取的方法又叫惰性加载,也叫流式读取,是处理大文件的一种常见方式。如果你的是文本文件,可以按行读取,那代码就更简单了:

with open('your_big_file.txt') as f:
    for line in f: 
        process_line(line)

Python3 文件类句柄就是一个迭代器,默认会将文件按行分割以惰性加载。

需要python全套入门学习资料的小伙伴私信领取即可

参考

  1. Python 3 术语对照表:docs.python.org/3/glossary.…

  2. Python 3 wiki:wiki.python.org/moin/Genera…

  3. Iterator vs Iterable: stackoverflow.com/questions/5…

  4. Lazy read big file:stackoverflow.com/questions/5…

最后,真心希望大家都能坚持下去,早日学会Python编程。


http://www.niftyadmin.cn/n/1399821.html

相关文章

用Python写了一个图像文字识别OCR工具

引言 最近在技术交流群里聊到一个关于图像文字识别的需求&#xff0c;在工作、生活中常常会用到&#xff0c;比如票据、漫画、扫描件、照片的文本提取。 博主基于 PyQt labelme PaddleOCR 写了一个桌面端的OCR工具&#xff0c;用于快速实现图片中文本区域自动检测文本自动识…

如何在 Python 中隐藏和加密密码?

有多种 Python 模块用于隐藏用户输入的密码&#xff0c;其中一个是**maskpass()模块。在 Python 中&#xff0c;借助maskpass()模块和base64()**模块&#xff0c;我们可以在输入时使用星号(*) 隐藏用户的密码&#xff0c;然后借助 base64() 模块可以对其进行加密。 maskpass()…

这几个用 Pyecharts 做出来的交互图表,领导说叼爆了

一、Pyecharts简介和安装 1、简介 Echarts 是一个由百度开源的数据可视化&#xff0c;凭借着良好的交互性&#xff0c;精巧的图表设计&#xff0c;得到了众多开发者的认可。而 Python 是一门富有表达力的语言&#xff0c;很适合用于数据处理。当数据分析遇上数据可视化时&…

Python的多线程为什么不能利用多核处理器

1.全局解释锁 如题: Python的多线程为什么不能利用多核处理器&#xff1f; 全局解释器锁(Global Interpreter Lock)是计算机程序设计语言解释器用于同步线程的一种机制&#xff0c;它使得任何时刻仅有一个线程在执行。 即便在多核处理器上&#xff0c;使用 GIL 的解释器也只…

你应该知道关于 python 的小技巧

今天分享一些使用的 python 小技巧&#xff0c;从接触 AI 用 python 也快 2 年了&#xff0c;也遇到问题&#xff0c;尝试一些解决方案来。感觉接下来介绍解决方案都是比较实用&#xff0c;也希望对您有所帮助。 两个列表之间的差 我们现在有两个列表分别是 models 和 object…

用Python写的一个2048小游戏,这可能就是程序员的乐趣吧

原理介绍 “使用方向键移动方块&#xff0c;两个数字相同的方块撞在一起后&#xff0c;将会合并为一个数字是原来两倍的新方块。游戏的时候尽可能多地合并这些数字方块就行了。” 大概了解了游戏规则之后&#xff0c;我们就可以开始写这个游戏啦~首先&#xff0c;进行一下游戏…

python用turtle画一个旋转的风车

上次用turtle做了一个爱心&#xff0c;这次我们来做一个旋转的风车&#xff0c;先放下图&#xff0c;看一下完成后的效果 现在我们开始代码的编写 设计叶子 一片叶子我们用一深一浅两个三角形就可以实现了&#xff0c;下面以0度方向的叶子为例&#xff0c;我们写出的代码如下…

看到室友用Python制作贪吃蛇小游戏,我直接偷偷学会

最近有很多的同学问&#xff0c;能不能用Python做出一个小游戏来&#xff0c;而且最好要讲清楚每一段干嘛是用来干嘛的 那行&#xff0c;今天将来讲解一下用Python pygame做一个贪吃蛇的小游戏 据说是贪吃蛇游戏是1976年&#xff0c;Gremlin公司推出的经典街机游戏&#xff0c…