Python學習教程(Python學習路線):給你們總結了兩個Python編寫循環的建議python
循環是一種經常使用的程序控制結構。咱們常說,機器相比人類的最大優勢之一,就是機器能夠不眠不休的重複作某件事情,但人卻不行。而「循環」,則是實現讓機器不斷重複工做的關鍵概念。編程
在循環語法方面,Python 表現的即傳統又不傳統。它雖然拋棄了常見的 for(init;condition;incrment) 三段式結構,但仍是選擇了 for 和 while 這兩個經典的關鍵字來表達循環。絕大多數狀況下,咱們的循環需求均可以用 for<item>in<iterable> 來知足, while<condition> 相比之下用的則更少些。數組
雖然循環的語法很簡單,可是要寫好它確並不容易。這裏,咱們將探討什麼是「地道」的循環代碼,以及如何編寫它們。bash
什麼是「地道」的循環?ide
「地道」這個詞,一般被用來形容某人作某件事情時,很是符合當地傳統,作的很是好。打個比方,你去參加一個朋友聚會,同桌的有一位廣東人,對方一開口,句句都是標準京腔、完美兒化音。那你能夠對她說:「您的北京話說的真地道」。函數
既然「地道」這個詞形容的常常是口音、作菜的口味這類實實在在的東西,那「地道」的循環代碼又是什麼意思呢?讓我拿一個經典的例子來解釋一下。工具
若是你去問一位剛學習 Python 一個月的人:「如何在遍歷一個列表的同時獲取當前下標?」。他可能會交出這樣的代碼:學習
index = 0
for name in names:
print(index, name)
index += 1
複製代碼
上面的循環雖然沒錯,但它確一點都不「地道」。一個擁有三年 Python 開發經驗的人會說,代碼應該這麼寫:測試
for i, name in enumerate(names):
print(i, name)
複製代碼
enumerate() 是 Python 的一個內置函數,它接收一個「可迭代」對象做爲參數,而後返回一個不斷生成 (當前下標,當前元素) 的新可迭代對象。這個場景使用它最適合不過。優化
因此,在上面的例子裏,咱們會認爲第二段循環代碼比第一段更「地道」。
由於它用更直觀的代碼,更聰明的完成了工做。
enumerate() 所表明的編程思路
不過,判斷某段循環代碼是否地道,並不只僅是以知道或不知道某個內置方法做爲標準。咱們能夠從上面的例子挖掘出更深層的東西。
如你所見,Python 的 for 循環只有 for<item>in<iterable> 這一種結構,而結構裏的前半部分 - 賦值給 item- 沒有太多花樣可玩。因此後半部分的可迭代對象是咱們惟一可以大作文章的東西。而以 enumerate() 函數爲表明的「修飾函數」,恰好提供了一種思路:經過修飾可迭代對象來優化循環自己。
這裏就想給你們說一下個人第一個建議了:
建議一:使用函數修飾被迭代對象來優化循環
使用修飾函數處理可迭代對象,能夠在各類方面影響循環代碼。而要找到合適的例子來演示這個方法,並不用去太遠,內置模塊 itertools 就是一個絕佳的例子。
簡單來講,itertools 是一個包含不少面向可迭代對象的工具函數集。我在以前的系列文章《容器的門道》裏提到過它。
若是要學習 itertools,那麼 Python 官方文檔 是你的首選,裏面有很是詳細的模塊相關資料。但在這篇文章裏,側重點將和官方文檔稍有不一樣。我會經過一些常見的代碼場景,來詳細解釋它是如何改善循環代碼的。
1. 使用 product 扁平化多層嵌套循環
雖然咱們都知道「扁平的代碼比嵌套的好」。但有時針對某類需求,彷佛必定得寫多層嵌套循環才行。好比下面這段:
def find_twelve(num_list1, num_list2, num_list3):
"""從 3 個數字列表中,尋找是否存在和爲 12 的 3 個數 """
for num1 in num_list1:
for num2 in num_list2:
for num3 in num_list3:
if num1 + num2 + num3 == 12:
return num1, num2, num3
複製代碼
對於這種須要嵌套遍歷多個對象的多層循環代碼,咱們可使用 product() 函數來優化它。product() 能夠接收多個可迭代對象,而後根據它們的笛卡爾積不斷生成結果。
from itertools import product
def find_twelve_v2(num_list1, num_list2, num_list3):
for num1, num2, num3 in product(num_list1, num_list2, num_list3):
if num1 + num2 + num3 == 12:
return num1, num2, num3
複製代碼
相比以前的代碼,使用 product() 的函數只用了一層 for 循環就完成了任務,代碼變得更精煉了。
2. 使用 islice 實現循環內隔行處理
有一份包含 Reddit 帖子標題的外部數據文件,裏面的內容格式是這樣的:
python-guide: Python best practices guidebook, written for humans.
---
Python 2 Death Clock
---
Run any Python Script with an Alexa Voice Command
---
<... ...>
複製代碼
多是爲了美觀,在這份文件裏的每兩個標題之間,都有一個 "---" 分隔符。如今,咱們須要獲取文件裏全部的標題列表,因此在遍歷文件內容的過程當中,必須跳過這些無心義的分隔符。
參考以前對 enumerate() 函數的瞭解,咱們能夠經過在循環內加一段基於當前循環序號的 if 判斷來作到這一點:
def parse_titles(filename):
"""從隔行數據文件中讀取 reddit 主題名稱 """
with open(filename, 'r') as fp:
for i, line in enumerate(fp):
# 跳過無心義的 '---' 分隔符
if i % 2 == 0:
yield line.strip()
複製代碼
但對於這類在循環內進行隔行處理的需求來講,若是使用 itertools 裏的 islice() 函數修飾被循環對象,可讓循環體代碼變得更簡單直接。
islice(seq,start,end,step) 函數和數組切片操做( list[start:stop:step] )有着幾乎如出一轍的參數。若是須要在循環內部進行隔行處理的話,只要設置第三個遞進步長參數 step 值爲 2 便可(默認爲 1)。
from itertools import islice
def parse_titles_v2(filename):
with open(filename, 'r') as fp:
# 設置 step=2,跳過無心義的 '---' 分隔符
for line in islice(fp, 0, None, 2):
yield line.strip()
複製代碼
3. 使用 takewhile 替代 break 語句
有時,咱們須要在每次循環開始時,判斷循環是否須要提早結束。好比下面這樣:
for user in users:
# 當第一個不合格的用戶出現後,再也不進行後面的處理
if not is_qualified(user):
break
# 進行處理 ... ...
複製代碼
對於這類須要提早中斷的循環,咱們可使用 takewhile() 函數來簡化它。takewhile(predicate,iterable)會在迭代 iterable 的過程當中不斷使用當前對象做爲參數調用 predicate 函數並測試返回結果,若是函數返回值爲真,則生成當前對象,循環繼續。不然當即中斷當前循環。
使用 takewhile 的代碼樣例:
from itertools import takewhile
for user in takewhile(is_qualified, users):
# 進行處理 ... ...
複製代碼
itertools 裏面還有一些其餘有意思的工具函數,他們均可以用來和循環搭配使用,好比使用 chain 函數扁平化雙層嵌套循環、使用 zip_longest 函數一次同時循環多個對象等等。
篇幅有限,我在這裏再也不一一介紹。若是有興趣,能夠自行去官方文檔詳細瞭解。
4. 使用生成器編寫本身的修飾函數
除了 itertools 提供的那些函數外,咱們還能夠很是方便的使用生成器來定義本身的循環修飾函數。
讓咱們拿一個簡單的函數舉例:
def sum_even_only(numbers):
"""對 numbers 裏面全部的偶數求和"""
result = 0
for num in numbers:
if num % 2 == 0:
result += num
return result
複製代碼
在上面的函數裏,循環體內爲了過濾掉全部奇數,引入了一條額外的 if 判斷語句。若是要簡化循環體內容,咱們能夠定義一個生成器函數來專門進行偶數過濾:
def even_only(numbers):
for num in numbers:
if num % 2 == 0:
yield num
def sum_even_only_v2(numbers):
"""對 numbers 裏面全部的偶數求和"""
result = 0
for num in even_only(numbers):
result += num
return result
複製代碼
將 numbers 變量使用 even_only 函數裝飾後, sum_even_only_v2 函數內部便不用繼續關注「偶數過濾」邏輯了,只須要簡單完成求和便可。
Hint:固然,上面的這個函數其實並不實用。在現實世界裏,這種簡單需求最適合直接用生成器/列表表達式搞定:sum(numfornuminnumbersifnum%2==0)
建議二:按職責拆解循環體內複雜代碼塊
我一直以爲循環是一個比較神奇的東西,每當你寫下一個新的循環代碼塊,就好像開闢了一片黑魔法陣,陣內的全部內容都會開始無休止的重複執行。
但我同時發現,這片黑魔法陣除了能帶來好處,它還會引誘你不斷往陣內塞入愈來愈多的代碼,包括過濾掉無效元素、預處理數據、打印日誌等等。甚至一些本來不屬於同一抽象的內容,也會被塞入到同一片黑魔法陣內。
你可能會以爲這一切理所固然,咱們就是迫切須要陣內的魔法效果。若是不把這一大堆邏輯塞滿到循環體內,還能把它們放哪去呢?
讓咱們來看看下面這個業務場景。在網站中,有一個每 30 天執行一次的週期腳本,它的任務是是查詢過去 30 天內,在每週末特定時間段登陸過的用戶,而後爲其發送獎勵積分。
代碼以下:
import time
import datetime
def award_active_users_in_last_30days():
"""獲取全部在過去 30 天週末晚上 8 點到 10 點登陸過的用戶,爲其發送獎勵積分 """
days = 30
for days_delta in range(days):
dt = datetime.date.today() - datetime.timedelta(days=days_delta)
# 5: Saturday, 6: Sunday
if dt.weekday() not in (5, 6):
continue
time_start = datetime.datetime(dt.year, dt.month, dt.day, 20, 0)
time_end = datetime.datetime(dt.year, dt.month, dt.day, 23, 0)
# 轉換爲 unix 時間戳,以後的 ORM 查詢須要
ts_start = time.mktime(time_start.timetuple())
ts_end = time.mktime(time_end.timetuple())
# 查詢用戶並挨個發送 1000 獎勵積分
for record in LoginRecord.filter_by_range(ts_start, ts_end):
# 這裏能夠添加複雜邏輯
send_awarding_points(record.user_id, 1000)
複製代碼
上面這個函數主要由兩層循環構成。外層循環的職責,主要是獲取過去 30 天內符合要求的時間,並將其轉換爲 UNIX 時間戳。以後由內層循環使用這兩個時間戳進行積分發送。
如以前所說,外層循環所開闢的黑魔法陣內被塞的滿滿當當。但經過觀察後,咱們能夠發現整個循環體實際上是由兩個徹底無關的任務構成的:「挑選日期與準備時間戳」 以及 「發送獎勵積分」。
複雜循環體如何應對新需求
這樣的代碼有什麼壞處呢?讓我來告訴你。
某日,產品找過來講,有一些用戶週末半夜不睡覺,還在刷咱們的網站,咱們得給他們發通知讓他們之後早點睡覺。因而新需求出現了:「給過去 30 天內在週末凌晨 3 點到 5 點登陸過的用戶發送一條通知」。
新問題也隨之而來。敏銳如你,確定一眼能夠發現,這個新需求在用戶篩選部分的要求,和以前的需求很是很是類似。可是,若是你再打開以前那團循環體看看,你會發現代碼根本無法複用,由於在循環內部,不一樣的邏輯徹底被耦合在一塊兒了。☹️
在計算機的世界裏,咱們常常用「耦合」這個詞來表示事物之間的關聯關係。上面的例子中,「挑選時間」和「發送積分」這兩件事情身處同一個循環體內,創建了很是強的耦合關係。
爲了更好的進行代碼複用,咱們須要把函數裏的「挑選時間」部分從循環體中解耦出來。而咱們的老朋友,「生成器函數」是進行這項工做的不二之選。
使用生成器函數解耦循環體
要把 「挑選時間」 部分從循環內解耦出來,咱們須要定義新的生成器函數 gen_weekend_ts_ranges(),專門用來生成須要的 UNIX 時間戳:
def gen_weekend_ts_ranges(days_ago, hour_start, hour_end):
"""生成過去一段時間內週六日特定時間段範圍,並以 UNIX 時間戳返回 """
for days_delta in range(days_ago):
dt = datetime.date.today() - datetime.timedelta(days=days_delta)
# 5: Saturday, 6: Sunday
if dt.weekday() not in (5, 6):
continue
time_start = datetime.datetime(dt.year, dt.month, dt.day, hour_start, 0)
time_end = datetime.datetime(dt.year, dt.month, dt.day, hour_end, 0)
# 轉換爲 unix 時間戳,以後的 ORM 查詢須要
ts_start = time.mktime(time_start.timetuple())
ts_end = time.mktime(time_end.timetuple())
yield ts_start, ts_end
複製代碼
有了這個生成器函數後,舊需求「發送獎勵積分」和新需求「發送通知」,就均可以在循環體內複用它來完成任務了:
def award_active_users_in_last_30days_v2():
"""發送獎勵積分"""
for ts_start, ts_end in gen_weekend_ts_ranges(30, hour_start=20, hour_end=23):
for record in LoginRecord.filter_by_range(ts_start, ts_end):
send_awarding_points(record.user_id, 1000)
def notify_nonsleep_users_in_last_30days():
"""發送通知"""
for ts_start, ts_end in gen_weekend_ts_range(30, hour_start=3, hour_end=6):
for record in LoginRecord.filter_by_range(ts_start, ts_end):
notify_user(record.user_id, 'You should sleep more')
複製代碼
總結
在這篇Python學習教程文章裏,咱們首先簡單解釋了「地道」循環代碼的定義。而後提出了第一個建議:使用修飾函數來改善循環。以後我虛擬了一個業務場景,描述了按職責拆解循環內代碼的重要性。
一些要點總結:
更多的Python學習教程和Python學習路線會繼續跟你們更新哦!或者你們有想學的相關Python學習教程也能夠留言哈!