日韩久久久精品,亚洲精品久久久久久久久久久,亚洲欧美一区二区三区国产精品 ,一区二区福利

Python編寫循環(huán)的兩個建議 | 鵝廠實戰(zhàn)

系統(tǒng) 1742 0

Python編寫循環(huán)的兩個建議 | 鵝廠實戰(zhàn)_第1張圖片


作者 | piglei(騰訊高級工程師)

轉(zhuǎn)載自騰訊技術(shù)工程知乎專欄


循環(huán)是一種常用的程序控制結(jié)構(gòu)。我們常說,機器相比人類的最大優(yōu)點之一,就是機器可以不眠不休的重復(fù)做某件事情,但人卻不行。而“ 循環(huán) ”,則是實現(xiàn)讓機器不斷重復(fù)工作的關(guān)鍵概念。


在循環(huán)語法方面,Python 表現(xiàn)的即傳統(tǒng)又不傳統(tǒng)。它雖然拋棄了常見的 for(init;condition;incrment) 三段式結(jié)構(gòu),但還是選擇了 for 和 while 這兩個經(jīng)典的關(guān)鍵字來表達循環(huán)。絕大多數(shù)情況下,我們的循環(huán)需求都可以用 for in 來滿足, while 相比之下用的則更少些。


雖然循環(huán)的語法很簡單,但是要寫好它確并不容易。在這篇文章里,我們將探討什么是“地道”的循環(huán)代碼,以及如何編寫它們。


什么是“地道”的循環(huán)?


“地道”這個詞,通常被用來形容某人做某件事情時,非常符合當?shù)貍鹘y(tǒng),做的非常好。打個比方,你去參加一個朋友聚會,同桌的有一位廣東人,對方一開口,句句都是標準京腔、完美兒化音。那你可以對她說:“您的北京話說的真 地道 ”。


既然“地道”這個詞形容的經(jīng)常是口音、做菜的口味這類實實在在的東西,那“地道”的循環(huán)代碼又是什么意思呢?讓我拿一個經(jīng)典的例子來解釋一下。


如果你去問一位剛學(xué)習 Python 一個月的人:“如何在遍歷一個列表的同時獲取當前下標?”。他可能會交出這樣的代碼:


              
            


上面的循環(huán)雖然沒錯,但它確一點都不“地道”。一個擁有三年 Python 開發(fā)經(jīng)驗的人會說,代碼應(yīng)該這么寫:

            


            

for ?i,? name ? in ?enumerate(names):
????print(i,? name )


enumerate() 是 Python 的一個內(nèi)置函數(shù),它接收一個“可迭代”對象作為參數(shù),然后返回一個不斷生成 (當前下標,當前元素) 的新可迭代對象。這個場景使用它最適合不過。


所以,在上面的例子里,我們會認為第二段循環(huán)代碼比第一段更“地道”。

因為它用更直觀的代碼,更聰明的完成了工作。


enumerate() 所代表的編程思路


不過,判斷某段循環(huán)代碼是否地道,并不僅僅是以知道或不知道某個內(nèi)置方法作為標準。我們可以從上面的例子挖掘出更深層的東西。


如你所見,Python 的 for 循環(huán)只有 for in 這一種結(jié)構(gòu),而結(jié)構(gòu)里的前半部分 - 賦值給 item- 沒有太多花樣可玩。所以后半部分的 可迭代對象 是我們唯一能夠大做文章的東西。而以 enumerate() 函數(shù)為代表的“修飾函數(shù)”,剛好提供了一種思路: 通過修飾可迭代對象來優(yōu)化循環(huán)本身。


這就引出了我的第一個建議。


建議1:使用函數(shù)修飾被迭代對象來優(yōu)化循環(huán)


使用修飾函數(shù)處理可迭代對象,可以在各種方面影響循環(huán)代碼。而要找到合適的例子來演示這個方法,并不用去太遠,內(nèi)置模塊 itertools 就是一個絕佳的例子。


簡單來說,itertools 是一個包含很多面向可迭代對象的工具函數(shù)集。我在之前的系列文章《容器的門道》里提到過它。


如果要學(xué)習 itertools,那么 Python 官方文檔 是你的首選,里面有非常詳細的模塊相關(guān)資料。但在這篇文章里,側(cè)重點將和官方文檔稍有不同。我會通過一些常見的代碼場景,來詳細解釋它是如何改善循環(huán)代碼的。


1. 使用 product 扁平化多層嵌套循環(huán)


雖然我們都知道“扁平的代碼比嵌套的好”。但有時針對某類需求,似乎一定得寫多層嵌套循環(huán)才行。比如下面這段:


            

def ? find_twelve (num_list1,?num_list2,?num_list3) :
???? """從?3?個數(shù)字列表中,尋找是否存在和為?12?的?3?個數(shù)
????"""

???? for ?num1? in ?num_list1:
???????? for ?num2? in ?num_list2:
???????????? for ?num3? in ?num_list3:
???????????????? if ?num1?+?num2?+?num3?==? 12 :
???????????????????? return ?num1,?num2,?num3


對于這種需要嵌套遍歷多個對象的多層循環(huán)代碼,我們可以使用 product() 函數(shù)來優(yōu)化它。product() 可以接收多個可迭代對象,然后根據(jù)它們的笛卡爾積不斷生成結(jié)果。


            

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() 的函數(shù)只用了一層 for 循環(huán)就完成了任務(wù),代碼變得更精煉了。


2. 使用 islice 實現(xiàn)循環(huán)內(nèi)隔行處理


有一份包含 Reddit 帖子標題的外部數(shù)據(jù)文件,里面的內(nèi)容格式是這樣的:


            

python-guide:?Python?best?practices?guidebook,?written? for ?humans.
---
Python? 2 ?Death?Clock
---
Run?any?Python?Script?with?an?Alexa?Voice?Command
---
<...?...>


可能是為了美觀,在這份文件里的每兩個標題之間,都有一個 "---" 分隔符。現(xiàn)在,我們需要獲取文件里所有的標題列表,所以在遍歷文件內(nèi)容的過程中,必須跳過這些無意義的分隔符。


參考之前對 enumerate() 函數(shù)的了解,我們可以通過在循環(huán)內(nèi)加一段基于當前循環(huán)序號的 if 判斷來做到這一點:


            

def ? parse_titles (filename) :
???? """從隔行數(shù)據(jù)文件中讀取?reddit?主題名稱
????"""

???? with ?open(filename,? 'r' )? as ?fp:
???????? for ?i,?line? in ?enumerate(fp):
???????????? #?跳過無意義的?'---'?分隔符
???????????? if ?i?%? 2 ?==? 0 :
???????????????? yield ?line.strip()


但對于這類在循環(huán)內(nèi)進行隔行處理的需求來說,如果使用 itertools 里的 islice() 函數(shù)修飾被循環(huán)對象,可以讓循環(huán)體代碼變得更簡單直接。


islice(seq,start,end,step) 函數(shù)和數(shù)組切片操作( list[start:stop:step] )有著幾乎一模一樣的參數(shù)。如果需要在循環(huán)內(nèi)部進行隔行處理的話,只要設(shè)置第三個遞進步長參數(shù) step 值為 2 即可(默認為 1)。

            


            

from ?itertools? import ?islice

def ? parse_titles_v2 (filename) :
???? with ?open(filename,? 'r' )? as ?fp:
???????? #?設(shè)置?step=2,跳過無意義的?'---'?分隔符
???????? for ?line? in ?islice(fp,? 0 ,? None ,? 2 ):
???????????? yield ?line.strip()


3. 使用 takewhile 替代 break 語句


有時,我們需要在每次循環(huán)開始時,判斷循環(huán)是否需要提前結(jié)束。比如下面這樣:


            

for ?user? in ?users:
???? #?當?shù)谝粋€不合格的用戶出現(xiàn)后,不再進行后面的處理
???? if ? not ?is_qualified(user):
???????? break

???? #?進行處理?...?...


對于這類需要提前中斷的循環(huán),我們可以使用 takewhile() 函數(shù)來簡化它。 takewhile(predicate,iterable) 會在迭代 ? iterable ? 的過程中不斷使用當前對象作為參數(shù)調(diào)用 ? predicate ? 函數(shù)并測試返回結(jié)果,如果函數(shù)返回值為真,則生成當前對象,循環(huán)繼續(xù)。否則立即中斷當前循環(huán)。


使用 takewhile 的代碼樣例:


            

from ?itertools? import ?takewhile

for ?user? in ?takewhile(is_qualified,?users):
???? #?進行處理?...?...


itertools 里面還有一些其他有意思的工具函數(shù),他們都可以用來和循環(huán)搭配使用,比如使用 chain 函數(shù)扁平化雙層嵌套循環(huán)、使用 zip_longest 函數(shù)一次同時循環(huán)多個對象等等。


篇幅有限,我在這里不再一一介紹。如果有興趣,可以自行去官方文檔詳細了解。


4. 使用生成器編寫自己的修飾函數(shù)


除了 itertools 提供的那些函數(shù)外,我們還可以非常方便的使用生成器來定義自己的循環(huán)修飾函數(shù)。


讓我們拿一個簡單的函數(shù)舉例:


            

def ? sum_even_only (numbers) :
???? """對?numbers?里面所有的偶數(shù)求和"""
????result?=? 0
???? for ?num? in ?numbers:
???????? if ?num?%? 2 ?==? 0 :
????????????result?+=?num
???? return ?result


在上面的函數(shù)里,循環(huán)體內(nèi)為了過濾掉所有奇數(shù),引入了一條額外的 if 判斷語句。如果要簡化循環(huán)體內(nèi)容,我們可以定義一個生成器函數(shù)來專門進行偶數(shù)過濾:


            

def ? even_only (numbers) :
???? for ?num? in ?numbers:
???????? if ?num?%? 2 ?==? 0 :
???????????? yield ?num

def ? sum_even_only_v2 (numbers) :
???? """對?numbers?里面所有的偶數(shù)求和"""
????result?=? 0
???? for ?num? in ?even_only(numbers):
????????result?+=?num
???? return ?result


將 numbers 變量使用 even_only 函數(shù)裝飾后, sum_even_only_v2 函數(shù)內(nèi)部便不用繼續(xù)關(guān)注“偶數(shù)過濾”邏輯了,只需要簡單完成求和即可。


Hint:當然,上面的這個函數(shù)其實并不實用。在現(xiàn)實世界里,這種簡單需求最適合直接用生成器/列表表達式搞定:sum(numfornuminnumbersifnum%2==0)


建議2:按職責拆解循環(huán)體內(nèi)復(fù)雜代碼塊


我一直覺得循環(huán)是一個比較神奇的東西,每當你寫下一個新的循環(huán)代碼塊,就好像開辟了一片黑魔法陣,陣內(nèi)的所有內(nèi)容都會開始無休止的重復(fù)執(zhí)行。


但我同時發(fā)現(xiàn),這片黑魔法陣除了能帶來好處, 它還會引誘你不斷往陣內(nèi)塞入越來越多的代碼,包括過濾掉無效元素、預(yù)處理數(shù)據(jù)、打印日志等等。甚至一些原本不屬于同一抽象的內(nèi)容,也會被塞入到同一片黑魔法陣內(nèi)。


你可能會覺得這一切理所當然,我們就是迫切需要陣內(nèi)的魔法效果。如果不把這一大堆邏輯塞滿到循環(huán)體內(nèi),還能把它們放哪去呢?


讓我們來看看下面這個業(yè)務(wù)場景。在網(wǎng)站中,有一個每 30 天執(zhí)行一次的周期腳本,它的任務(wù)是是查詢過去 30 天內(nèi),在每周末特定時間段登錄過的用戶,然后為其發(fā)送獎勵積分。


代碼如下:


            

import ?time
import ?datetime

def ? award_active_users_in_last_30days () :
???? """獲取所有在過去?30?天周末晚上?8?點到?10?點登錄過的用戶,為其發(fā)送獎勵積分
????"""

????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 )

???????? #?轉(zhuǎn)換為?unix?時間戳,之后的?ORM?查詢需要
????????ts_start?=?time.mktime(time_start.timetuple())
????????ts_end?=?time.mktime(time_end.timetuple())

???????? #?查詢用戶并挨個發(fā)送?1000?獎勵積分
???????? for ?record? in ?LoginRecord.filter_by_range(ts_start,?ts_end):
???????????? #?這里可以添加復(fù)雜邏輯
????????????send_awarding_points(record.user_id,? 1000 )?


上面這個函數(shù)主要由兩層循環(huán)構(gòu)成。外層循環(huán)的職責,主要是獲取過去 30 天內(nèi)符合要求的時間,并將其轉(zhuǎn)換為 UNIX 時間戳。之后由內(nèi)層循環(huán)使用這兩個時間戳進行積分發(fā)送。


如之前所說,外層循環(huán)所開辟的黑魔法陣內(nèi)被塞的滿滿當當。但通過觀察后,我們可以發(fā)現(xiàn) 整個循環(huán)體其實是由兩個完全無關(guān)的任務(wù)構(gòu)成的:“挑選日期與準備時間戳” 以及 “發(fā)送獎勵積分 ”。


復(fù)雜循環(huán)體如何應(yīng)對新需求


這樣的代碼有什么壞處呢?讓我來告訴你。


某日,產(chǎn)品找過來說,有一些用戶周末半夜不睡覺,還在刷我們的網(wǎng)站,我們得給他們發(fā)通知讓他們以后早點睡覺。于是新需求出現(xiàn)了:“ 給過去 30 天內(nèi)在周末凌晨 3 點到 5 點登錄過的用戶發(fā)送一條通知”


新問題也隨之而來。敏銳如你,肯定一眼可以發(fā)現(xiàn),這個新需求在用戶篩選部分的要求,和之前的需求非常非常相似。但是,如果你再打開之前那團循環(huán)體看看,你會發(fā)現(xiàn)代碼根本沒法復(fù)用,因為在循環(huán)內(nèi)部,不同的邏輯完全被 耦合 在一起了。??


在計算機的世界里,我們經(jīng)常用“ 耦合 ”這個詞來表示事物之間的關(guān)聯(lián)關(guān)系。上面的例子中,“挑選時間”和“發(fā)送積分”這兩件事情身處同一個循環(huán)體內(nèi),建立了非常強的耦合關(guān)系。


為了更好的進行代碼復(fù)用,我們需要把函數(shù)里的“挑選時間”部分從循環(huán)體中解耦出來。而我們的老朋友,“ 生成器函數(shù) ”是進行這項工作的不二之選。


使用生成器函數(shù)解耦循環(huán)體


要把 “挑選時間” 部分從循環(huán)內(nèi)解耦出來,我們需要定義新的生成器函數(shù) gen_weekend_ts_ranges(),專門用來生成需要的 UNIX 時間戳:


            

def ? gen_weekend_ts_ranges (days_ago,?hour_start,?hour_end) :
???? """生成過去一段時間內(nèi)周六日特定時間段范圍,并以?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 )

???????? #?轉(zhuǎn)換為?unix?時間戳,之后的?ORM?查詢需要
????????ts_start?=?time.mktime(time_start.timetuple())
????????ts_end?=?time.mktime(time_end.timetuple())
???????? yield ?ts_start,?ts_end


有了這個生成器函數(shù)后,舊需求“發(fā)送獎勵積分”和新需求“發(fā)送通知”,就都可以在循環(huán)體內(nèi)復(fù)用它來完成任務(wù)了:


            

def ? award_active_users_in_last_30days_v2 () :
???? """發(fā)送獎勵積分"""
???? 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 () :
???? """發(fā)送通知"""
???? 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' )

            

總結(jié)


在這篇文章里,我們首先簡單解釋了“地道”循環(huán)代碼的定義。然后提出了第一個建議:使用修飾函數(shù)來改善循環(huán)。之后我虛擬了一個業(yè)務(wù)場景,描述了按職責拆解循環(huán)內(nèi)代碼的重要性。


一些要點總結(jié):


  • 使用函數(shù)修飾被循環(huán)對象本身,可以改善循環(huán)體內(nèi)的代碼

  • itertools 里面有很多工具函數(shù)都可以用來改善循環(huán)

  • 使用生成器函數(shù)可以輕松定義自己的修飾函數(shù)

  • 循環(huán)內(nèi)部,是一個極易發(fā)生“代碼膨脹”的場地

  • 請使用生成器函數(shù)將循環(huán)內(nèi)不同職責的代碼塊解耦出來,獲得更好的靈活性


看完文章的你,有沒有什么想吐槽的?請留言或者在 項目 Github Issues 告訴我吧。


附錄


  • 題圖來源: Photo by Lai man nung on Unsplash

  • 更多系列文章地址:https://github.com/piglei/one-python-craftsman


(*本文為 AI科技大本營轉(zhuǎn)載文章,轉(zhuǎn)載請聯(lián)系原作者)


精彩推薦



Python編寫循環(huán)的兩個建議 | 鵝廠實戰(zhàn)_第2張圖片


6月29-30日 ,2019以太坊技術(shù)及應(yīng)用大會 特邀 以太坊創(chuàng)始人V神與以太坊基金會核心成員 ,以及海內(nèi)外知名專家齊聚北京,聚焦前沿技術(shù),把握時代機遇,深耕行業(yè)應(yīng)用,共話以太坊2.0新生態(tài)。


掃碼或點擊閱讀原文,既享優(yōu)惠購票!


Python編寫循環(huán)的兩個建議 | 鵝廠實戰(zhàn)_第3張圖片


推薦閱讀

  • Bert時代的創(chuàng)新:Bert在NLP各領(lǐng)域的應(yīng)用進展 | 技術(shù)頭條

  • 免費GPU哪家強?谷歌Kaggle vs. Colab

  • 高能!8段代碼演示Numpy數(shù)據(jù)運算的神操作

  • Python編寫循環(huán)的兩個建議 | 鵝廠實戰(zhàn)

  • Lambda 表達式有何用處?

  • 9年前他用1萬個比特幣買了兩個披薩, 9年后他把當年的代碼賣給了蘋果,成為了 GPU 挖礦之父

  • TIOBE 6月編程語言排行榜:Python 勢不可擋,或在四年之內(nèi)超越Java、C

  • 漫威金剛狼男主棄影炒幣了?



640?wx_fmt=png 你點的每個“在看”,我都認真當成了喜歡

更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主

微信掃碼或搜索:z360901061

微信掃一掃加我為好友

QQ號聯(lián)系: 360901061

您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。

【本文對您有幫助就好】

您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描上面二維碼支持博主2元、5元、10元、自定義金額等您想捐的金額吧,站長會非常 感謝您的哦!!!

發(fā)表我的評論
最新評論 總共0條評論
主站蜘蛛池模板: 容城县| 普兰店市| 贞丰县| 尉犁县| 兴业县| 巴里| 扶余县| 浦东新区| 鞍山市| 乌兰浩特市| 噶尔县| 成都市| 海淀区| 临湘市| 成安县| 贺州市| 当雄县| 岱山县| 武冈市| 图木舒克市| 汉川市| 三都| 林甸县| 岑巩县| 闻喜县| 汤原县| 佛学| 连云港市| 新蔡县| 安义县| 淮北市| 农安县| 山西省| 阳高县| 格尔木市| 南充市| 齐齐哈尔市| 通江县| 定远县| 江津市| 扎赉特旗|