寫過一段時間代碼的同窗,應該對這一句話深有體會:程序的時間利用率和空間利用率每每是矛盾的,能夠用時間換空間,能夠用空間換時間,但很難同時提升一個程序的時間利用率和空間利用率。python
但若是你嘗試使用生成器來重構你的代碼,也許你會發現,在必定程度上,你能夠既提升時間利用率,又提升空間利用率。redis
咱們以一個數據清洗的簡單項目爲例,來講明生成器如何讓你的代碼運行起來更加高效。bash
在 Redis 中,有一個列表
datalist
,裏面有不少的數據,這些數據多是純阿拉伯數字
,中文數字
,字符串"敏感信息"
。如今咱們須要實現:從 Redis 中讀取全部的數據,把全部的字符串敏感信息
所有丟掉,把全部中文數字所有轉換爲阿拉伯數字,以{'num': 12345, 'date': '2019-10-30 18:12:14'}
這樣的格式插入到 MongoDB 中。app
示例數據以下:函數
41234213424
一九八八七二六三
8394520342
七二三六二九六六
敏感信息
80913408120934
敏感信息
敏感信息
95352345345
三三七四六
999993232
234234234
三六八八七七
敏感信息
複製代碼
以下圖所示:編碼
![](http://static.javashuo.com/static/loading.gif)
若是讓你來寫這個轉換程序,你可能會這樣寫: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
![](http://static.javashuo.com/static/loading.gif)
這段代碼,看起來很 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}')
其運行效果以下圖所示:
![](http://static.javashuo.com/static/loading.gif)
你們對比上面兩張插圖。前一張插圖,先生成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)
代碼以下圖所示:
![](http://static.javashuo.com/static/loading.gif)
若是你開啓 PyCharm 調試模式,你會發現,數據的流向是這樣的:
- 從 Redis 獲取1條數據
- 這一條數據傳給remove_sensitive_data
- 第2步處理之後的數據傳給tranfer_chinese_num
- 第3步處理之後,傳給 save_data
- 回到第1步
整個過程就像是一條流水線同樣,數據一條一條地進行處理和存檔。不需建立額外的列表,有多少條數據就循環多少次,不作多餘的循環。