

写了那么久的Python,你应该学会使用yield关键字了
source link: https://www.tuicool.com/articles/YjyAnmN
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

写过一段时间代码的同学,应该对这一句话深有体会:程序的时间利用率和空间利用率往往是矛盾的,可以用时间换空间,可以用空间换时间,但很难同时提高一个程序的时间利用率和空间利用率。
但如果你尝试使用生成器来重构你的代码,也许你会发现,在一定程度上,你可以既提高时间利用率,又提高空间利用率。
我们以一个数据清洗的简单项目为例,来说明生成器如何让你的代码运行起来更加高效。
在 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 调试模式,你会发现,数据的流向是这样的:
- 从 Redis 获取1条数据
- 这一条数据传给remove_sensitive_data
- 第2步处理以后的数据传给tranfer_chinese_num
- 第3步处理以后,传给 save_data
- 回到第1步
整个过程就像是一条流水线一样,数据一条一条地进行处理和存档。不需创建额外的列表,有多少条数据就循环多少次,不做多余的循环。
Recommend
-
51
前言 之前的一篇文章《你应该学会的Postman用法》,主要介绍了postman的一些高级的用法,便于日常开发和调试使用,本文的基础是对postman的基本使用以及一些高级用法有一定的了解,如对此不太了解的同学,建议移步:《你应该学会的Postman用法》了解
-
57
有问题,上知乎。知乎是中文互联网知名知识分享平台,以「知识连接一切」为愿景,致力于构建一个人人都可以便捷接入的知识分享网络,让人们便捷地与世界分享知识、经验和见解,发现更大的世界。
-
38
程序员 - @cmower - 最近查了一下搜索的关键字,Python 相关的遥遥领先,比 Java 关键字要多出来 2000 次,本周的数据。非常震惊,搜索 pycharm 激活码的次数更是遥遥领先啊。难道要学 P
-
50
继上篇文章 原创 | 全网最新最简单的 openjdk13 代码编译 之后,我们有了...
-
7
“哥,一周过去了,教妹学 Java 你都没有更新,偷懒了呀!”三妹关心地问我。 “今天就更新。”我面带着微笑对三妹说,“学习可不能落下,今天我们来学 Java 中 static 关键字吧。” “static 是 Java 中比较难以理解的一个关...
-
11
最近在研究 tensorflow框架下的深度学习,了解到深度学习对数据非常依赖,而标注好的数据集非常难得。今天发现 python 的 captcha 库,可以生成验证码,因...
-
4
神译局是36氪旗下编译团队,关注科技、商业、职场、生活等领域,重点介绍国外的新技术、新观点、新风向。 编者按:近年来,Python已经取代Java成为高校中最受欢迎的编程语言,甚至还有许多不做编程工作的人士,也在开始在学习Py...
-
7
在比特币大跌的时候 我们应该学会什么? # 市场要闻 2021-05-17 15:52
-
5
学会游泳后,危急情况时下水救人应该注意些什么?iNote/学会游泳后,危急情况时下水救人应该注意些什么?Search
-
5
2023-12-25 01:56 有了 AI 之后,成熟的手机应该学会自己工作了 原文来源:品玩
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK