神奇的yield

 

写了那么久的Python,你应该学会使用yield关键字了

写过一段时间代码的同窗,应该对这一句话深有体会:程序的时间利用率和空间利用率每每是矛盾的,能够用时间换空间,能够用空间换时间,但很难同时提升一个程序的时间利用率和空间利用率。python

但若是你尝试使用生成器来重构你的代码,也许你会发现,在必定程度上,你能够既提升时间利用率,又提升空间利用率。redis

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

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

示例数据以下:函数

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

以下图所示:编码

 

 

 

 

若是让你来写这个转换程序,你可能会这样写:spa

 

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)
 

运行效果以下图所示:3d

 

 

 

 

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

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

也许你会说,你能够把移除敏感信息,中文数字转阿拉伯数字的逻辑所有写在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步

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

相关文章
相关标签/搜索