重新认识生成器Generator

开发 前端
我们知道,函数体包含 yield 关键字的函数不是一个普通函数。这种函数叫做 生成器 ( generator ),一般用于循环处理结构,应用得当可以极大优化内存使用效率。

 [[395138]]

本文转载自微信公众号「小菜学编程」,作者fasionchan。转载本文请联系小菜学编程公众号。

基本用法

我们知道,函数体包含 yield 关键字的函数不是一个普通函数。这种函数叫做 生成器 ( generator ),一般用于循环处理结构,应用得当可以极大优化内存使用效率。例如,设计一个函数,打开文件并将每一行转成大写并返回:

  1. def read_file_upper(path): 
  2.     lines = [] 
  3.     with open(path) as f: 
  4.         for line in f: 
  5.             lines.append(line.upper()) 
  6.     return lines 

这个版本的函数,在内部创建了一个 list 对象,用于保存转换结果。for 循环则遍历文件每一行,将其转成大写并追加到列表中。这样一来,文件中的每一行均需要保存在列表中,如果文件很大,内存开销可想而知。

我们可以借助 yield 关键字,将 read_file_upper 函数改成生成器版本。函数主体逻辑没有任何变化,只是将每行数据的处理结果通过 yield 逐个返回,而不是收集到 list 对象后再返还。

  1. def iter_file_upper(path): 
  2.     with open(path) as f: 
  3.         for line in f: 
  4.             yield line.upper() 

如果现在有一个文本文件 data.txt ,里面包含以下内容:

  1. hello, world 
  2. life is short, use python 
  3. my wechat id is: coding-fan 
  4. bye 

用 iter_file_upper 生成器,我们可以这样对它进行处理:

  1. >>> for line in iter_file_upper('text.txt'): 
  2. ...     print(line.strip()) 
  3. HELLO, WORLD 
  4. LIFE IS SHORT, USE PYTHON 
  5. MY WECHAT ID IS: CODING-FAN 
  6. BYE 

iter_file_upper 生成器用法与 read_file_upper 函数大致相同,但它不会一次性拿住文件所有数据行,而是逐行处理、逐个返回,这样便将内存使用量降到最低。

行为观察

那么,生成器为什么会有这样的奇效呢?我们接着观察:

  1. >>> g = iter_file_upper('text.txt'
  2. >>> g 
  3. <generator object iter_file_upper at 0x103becd68> 

我们调用 iter_file_upper 后,得到一个生成器对象,而不是文件处理结果,这时 iter_file_upper 还未开始执行。

当我们调用 next 函数从生成器接收下一个数据时,iter_file_upper 开始执行并在 yield 处停下来,并把第一行的处理结果返回给我们:

  1. >>> next(g) 
  2. 'HELLO, WORLD\n' 

这时,生成器处于暂停状态,没有我们的指令,它不会接着处理第二行数据。

当我们再次执行 next 函数时,生成器再次恢复执行,处理下一行数据并在 yield 处再次暂停:

  1. >>> next(g) 
  2. 'LIFE IS SHORT, USE PYTHON\n' 

生成器记住了自己的执行进度,每次调用 next 函数,它总是处理并生产下一个数据,完全不用我们瞎操心:

  1. >>> next(g) 
  2. 'MY WECHAT ID IS: CODING-FAN\n' 
  3. >>> next(g) 
  4. 'BYE\n' 

当 iter_file_upper 代码逻辑执行完毕,它将给 next 抛一个异常,以此通知调用者它已经结束了:

  1. >>> next(g) 
  2. Traceback (most recent call last): 
  3.   File "<stdin>", line 1, in <module> 
  4. StopIteration 

因此,我们可以简单认为 for-in 循环在 Python 虚拟机内部是这样实现的:

  • 不断调用 next 函数让生成器产出数据;
  • 直到生成器抛出 StopIteration 异常;

在经典的线程模型中,每个线程有一个独立的执行流,只能执行一个任务。如果一个程序需要同时处理多个任务,可以借助 多进程 或者 多线程 技术。假设一个站点需要同时服务多个客户端连接,可以为每个连接创建一个独立的线程进行处理。

不管线程还是进程,切换时都会带来巨大的开销:用户态/内核态切换、执行上下文保存和恢复、CPU缓存刷新等等。因此,用线程或进程来驱动小任务的执行,显然不是一个理想的选择。

那么,除了线程和进程,还有其他解决方案吗?

 

责任编辑:武晓燕 来源: 小菜学编程
相关推荐

2022-03-03 08:30:41

GeneratorES6函数

2014-01-06 11:23:54

Mesos设计架构

2019-02-24 21:27:26

物联网网关物联网IOT

2019-10-31 13:40:52

JavaPHP编程语言

2016-12-13 15:41:40

JavaHashMap

2016-11-07 11:34:28

数据可视化大数据

2020-09-17 07:08:04

TypescriptVue3前端

2019-09-02 08:53:46

程序员

2021-11-11 05:00:02

JavaMmap内存

2022-10-27 13:58:32

Python编程生成器

2017-01-03 17:22:16

公共云安全

2009-08-28 10:40:18

开源Linux防火墙生成器Linux操作系统

2011-12-23 13:42:05

JavaScript

2010-09-07 16:31:17

SQL语句insert

2012-01-11 09:12:25

程序员

2010-10-22 11:10:24

软考

2015-03-19 10:15:54

程序员价值程序员价值

2022-10-09 11:46:55

机器人人工智能

2019-04-15 14:32:11

2019-01-18 13:32:16

点赞
收藏

51CTO技术栈公众号