写过一段时间代码的同学,应该对这一句话深有体会:程序的时间利用率和空间利用率往往是矛盾的,可以用时间换空间,可以用空间换时间,但很难同时提高一个程序的时间利用率和空间利用率。

但如果你尝试使用生成器来重构你的代码,也许你会发现,在一定程度上,你可以既提高时间利用率,又提高空间利用率。

我们以一个数据清洗的简单项目为例,来说明生成器如何让你的代码运行起来更加高效。

在 Redis 中,有一个列表datalist,里面有很多的数据,这些数据可能是纯阿拉伯数字中文数字字符串"敏感信息"。现在我们需要实现:从 Redis 中读取所有的数据,把所有的字符串敏感信息全部丢掉,把所有中文数字全部转换为阿拉伯数字,以{'num': 12345, 'date': '2019-10-30 18:12:14'}这样的格式插入到 MongoDB 中。

示例数据如下:

41234213424
一九八八七二六三
8394520342
七二三六二九六六
敏感信息
80913408120934
敏感信息
敏感信息
95352345345
三三七四六
999993232
234234234
三六八八七七
敏感信息

如下图所示:

如果让你来写这个转换程序,你可能会这样写:

import redis
import datetime
import pymongo client = redis.Redis()
handler = pymongo.MongoClient().data_list.num CHINESE_NUM_DICT = {
'一': '1',
'二': '2',
'三': '3',
'四': '4',
'五': '5',
'六': '6',
'七': '7',
'八': '8',
'九': '9'
} def get_data():
datas = []
while True:
data = client.lpop('datalist')
if not data:
break
datas.append(data.decode())
return datas def remove_sensitive_data(datas):
clear_data = []
for data in datas:
if data == '敏感信息':
continue
clear_data.append(data)
return clear_data def tranfer_chinese_num(datas):
number_list = []
for data in datas:
try:
num = int(data)
except ValueError:
num = ''.join(CHINESE_NUM_DICT[x] for x in data)
number_list.append(num)
return number_list def save_data(number_list):
for number in number_list:
data = {'num': number, 'date': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
handler.insert_one(data) raw_data = get_data()
safe_data = remove_sensitive_data(raw_data)
number_list = tranfer_chinese_num(safe_data)
save_data(number_list)

运行效果如下图所示:

这段代码,看起来很 Pythonic,一个函数只做一件事,看起来也满足编码规范。最后运行结果也正确。能有什么问题?

问题在于,这段代码,每个函数都会创建一个列表存放处理以后的数据。如果 Redis 中的数据多到超过了你当前电脑的内存怎么办?对同一批数据多次使用 for 循环,浪费了大量的时间,能不能只循环一次?

也许你会说,你可以把移除敏感信息,中文数字转阿拉伯数字的逻辑全部写在get_data函数的 while循环中,这样不就只循环一次了吗?

可以是可以,但是这样一来,get_data就做了不止一件事情,代码也显得非常混乱。如果以后要增加一个新的数据处理逻辑:

转换为数字以后,检查所有奇数位的数字相加之和与偶数位数字相加之和是否相等,丢弃所有相等的数字。

那么你就要修改get_data的代码。

在开发软件的时候,我们应该面向扩展开放,面向修改封闭,所以不同的逻辑,确实应该分开,所以上面把每个处理逻辑分别写成函数的写法,在软件工程上没有问题。但是如何做到处理逻辑分开,又不需要对同一批数据进行多次 for 循环呢?

这个时候,就要依赖于我们的生成器了。

我们先来看看下面这一段代码的运行效果:

def gen_num():
nums = []
for i in range(10):
print(f'生成数据:{i}')
nums.append(i)
return nums
nums = gen_num()
for num in nums:
print(f'打印数据:{num}')

运行效果如下图所示:

现在,我们对代码做一下修改:

def gen_num():
for i in range(10):
print(f'生成数据:{i}')
yield i
nums = gen_num()
for num in nums:
print(f'打印数据:{num}')

其运行效果如下图所示:

大家对比上面两张插图。前一张插图,先生成10个数据,然后再打印10个数据。后一张图,生成一个数据,打印一个数据,再生成一个数据,再打印一个数据……

如果以代码的行号来表示运行运行逻辑,那么代码是按照这个流程运行的:

1->5->6->2->3->4->6->7->6->2->3->4->6->7->6->2->3->4->6->7....

大家可以把这段代码写在 PyCharm 中,然后使用单步调试来查看它每一步运行的是哪一行代码。

程序运行到yield就会把它后面的数字抛出到外面给 for 循环, 然后进入外面 for 循环的循环体,外面的 for 循环执行完成后,又会进入gen_num函数里面的 yield i后面的一行,开启下一次 for 循环,继续生成新的数字……

整个过程中,不需要额外创建一个列表来保存中间的数据,从而达到节约内存空间的目的。而整个过程中,虽然代码写了两个 for 循环,但是如果你使用单步调试,你就会发现实际上真正的循环只有for i in range(10)。而外面的for num in nums仅仅是实现了函数内外的切换,并没有新增循环。

回到最开始的问题,我们如何使用生成器来修改代码呢?实际上你几乎只需要把return 列表改成yield 每一个元素即可:

import redis
import datetime
import pymongo client = redis.Redis()
handler = pymongo.MongoClient().data_list.num_yield CHINESE_NUM_DICT = {
'一': '1',
'二': '2',
'三': '3',
'四': '4',
'五': '5',
'六': '6',
'七': '7',
'八': '8',
'九': '9'
} def get_data():
while True:
data = client.lpop('datalist')
if not data:
break
yield data.decode() def remove_sensitive_data(datas):
for data in datas:
if data == '敏感信息':
continue
yield data def tranfer_chinese_num(datas):
for data in datas:
try:
num = int(data)
except ValueError:
num = ''.join(CHINESE_NUM_DICT[x] for x in data)
yield num def save_data(number_list):
for number in number_list:
data = {'num': number, 'date': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
handler.insert_one(data) raw_data = get_data()
safe_data = remove_sensitive_data(raw_data)
number_list = tranfer_chinese_num(safe_data)
save_data(number_list)

代码如下图所示:

如果你开启 PyCharm 调试模式,你会发现,数据的流向是这样的:

  1. 从 Redis 获取1条数据
  2. 这一条数据传给remove_sensitive_data
  3. 第2步处理以后的数据传给tranfer_chinese_num
  4. 第3步处理以后,传给 save_data
  5. 回到第1步

整个过程就像是一条流水线一样,数据一条一条地进行处理和存档。不需创建额外的列表,有多少条数据就循环多少次,不做多余的循环。

写了那么久的Python,你应该学会使用yield关键字了的更多相关文章

  1. 写给想要入门python或者正在入门python的小朋友们

    写在前面: 最近好像python挺火,虽然我也在天天写python,但是python毕竟是动态语言,就拿常被人吐槽的java来说,python绝大不多数地方是不如java的.python只能是你的一个 ...

  2. 写给.NET开发者的Python教程(一):引言

    距离上一篇博文已过去8个月了,这段时间发生了很多事情导致没能持续更新博客.这段时间除了工作繁忙,业余时间都投入到AI技术的学习中,后面一段时间将会给大家分享我作为一个.NET开发人员在深度学习领域学习 ...

  3. (转) Python Generators(生成器)——yield关键字

    http://blog.csdn.net/scelong/article/details/6969276 生成器是这样一个函数,它记住上一次返回时在函数体中的位置.对生成器函数的第二次(或第 n 次) ...

  4. [Python 学习]2.5版yield之学习心得 - limodou的学习记录 - limodou是一个程序员,他关心的焦点是Python, DocBook, Open Source …

    [Python 学习]2.5版yield之学习心得 - limodou的学习记录 - limodou是一个程序员,他关心的焦点是Python, DocBook, Open Source - [Pyth ...

  5. Python入门之迭代器/生成器/yield的表达方式/面向过程编程

    本章内容 迭代器 面向过程编程 一.什么是迭代 二.什么是迭代器 三.迭代器演示和举例 四.生成器yield基础 五.生成器yield的表达式形式 六.面向过程编程 ================= ...

  6. python yield 关键字

    最近看代码看到python里面的yield关键字,和我之前接触的语言好像都没有来着,我就查了查它的含义,大概理解如下: >>> def createGenerator(): ... ...

  7. Python:容器、迭代对象、迭代器、生成器及yield关键字

            在了解Python的数据结构时,容器(container).可迭代对象(iterable).迭代器(iterator).生成器(generator).列表/集合/字典推导式(list, ...

  8. python中的yield关键字

    yield关键字一直困扰了我很久,一直也没有弄明白,现在将暂时理解的yield记录如下,供参考: 关键词:可迭代对象,生成器,迭代器 一.可迭代对象: 可迭代对象:可迭代对象是一个泛称,只要可以用fo ...

  9. 【Python注意事项】如何理解python中间generator functions和yield表情

    本篇记录自己的笔记Python的generator functions和yield理解表达式. 1. Generator Functions Python支持的generator functions语 ...

随机推荐

  1. 新手学习FFmpeg - 调用API完成视频的读取和输出

    在写了几个avfilter之后,原本以为对ffmpeg应该算是入门了. 结果今天想对一个视频文件进行转码操作,才发现基本的视频读取,输出都搞不定. 痛定思痛,仔细研究了一下ffmpeg提供的examp ...

  2. Tomcat 报错 The APR based Apache Tomcat Native library which allows optimal performance in production environmen

    这个问题在我一次重新装了tomcat和myeclipse时出现 说实话 出现这个问题头大 但是好在解决了 美滋滋 最开始到处寻找各种解决方案 最后直接注释了server.xml中的一行 直接解决这个报 ...

  3. JS的运动1(从简单到复杂运动,从单一属性到多属性同时进行的运动过程分析)

    js运动原理 运动基础 在js中,让一个元素动起来的最简单的方式,就是点击按钮,让元素移动.下面是一个简单的案例:(下面几个案例的的html和css都是采用这个为例) <!DOCTYPE htm ...

  4. 常见MySQL数据库语句

    ##############Author: Fan ################# (1)数据库    # 查看所有的数据库    SHOW DATABASES ;    # 创建一个数据库   ...

  5. Spring 梳理-AOP

    界面应用场景 日志.声明式事务.安全.缓存 AOP功能演化图 图片引用地址:https://www.cnblogs.com/best/p/5679656.html AOP设计模式-代理模式 静态代理: ...

  6. Idea 设置Eclipse快捷键(常用)

    使用Idea不习惯,特此将其配置成Eclipse风格的. 1.选择Eclipse风格,选择copy一份,可以自己重命名. 2.设置生成快捷键的快捷键(例如:Eclipse中的Alt+/) 3.设置ma ...

  7. 无法导入要素类到SDE中

    我遇到的原因的表空间不足(并且表空间没有设置为自动增长) 首先通过SELECT FILE_NAME, TABLESPACE_NAME, AUTOEXTENSIBLE FROM DBA_DATA_FIL ...

  8. C# 服务里面调用Python.exe 来执行python文件

    问题描述:在WCF服务里面通过调用python.exe来执行py文件,像下面这样py文件路径+参数,用空格隔开.会出现调用结果为空的现象 System.Diagnostics.ProcessStart ...

  9. centos7.x 将普通用户加入sudoers中,获取sudo执行权限

    centos7.x 将普通用户加入sudoers中,获取sudo执行权限 0 问题由来   你在提起兴致学习的时候,兴冲冲地往黑洞洞的文本窗口输入下一行: [pan@localhost ~]$ sud ...

  10. ShutdownHook- Java 优雅停机解决方案

    想象一下,如果你现在刚好在 word 上写需求文档,电脑突然重启.等待开机完成,你可能会发现写了一个小时文档没有保存,就这么没了... 一个正在运行 Java 应用如果突然将其停止,影响不止数据丢失, ...