最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會(huì)員登陸 & 注冊(cè)

Python 工匠:做一個(gè)精通規(guī)則的玩家

2022-08-12 16:48 作者:騰訊藍(lán)鯨智云  | 我要投稿

前言

編程,其實(shí)和玩電子游戲有一些相似之處。你在玩不同游戲前,需要先學(xué)習(xí)每個(gè)游戲的不同規(guī)則,只有熟悉和靈活運(yùn)用游戲規(guī)則,才更有可能在游戲中獲勝。

而編程也是一樣,不同編程語言同樣有著不一樣的“規(guī)則”。大到是否支持面向?qū)ο?,小到是否可以定義常量,編程語言的規(guī)則比絕大多數(shù)電子游戲要復(fù)雜的多。

當(dāng)我們編程時(shí),如果直接拿一種語言的經(jīng)驗(yàn)套用到另外一種語言上,很多時(shí)候并不能取得最佳結(jié)果。這就好像一個(gè) CS(反恐精英) 高手在不了解規(guī)則的情況下去玩 PUBG(絕地求生),雖然他的槍法可能萬中無一,但是極有可能在發(fā)現(xiàn)第一個(gè)敵人前,他就會(huì)倒在某個(gè)窩在草叢里的敵人的伏擊下。

Python 里的規(guī)則

Python 是一門初見簡(jiǎn)單、深入后愈覺復(fù)雜的語言。拿 Python 里最重要的“對(duì)象”概念來說,Python 為其定義了多到讓你記不全的規(guī)則,比如:

定義了 __str__ 方法的對(duì)象,就可以使用 str() 函數(shù)來返回可讀名稱

定義了 __next__ 和 __iter__ 方法的對(duì)象,就可以被循環(huán)迭代

定義了 __bool__ 方法的對(duì)象,在進(jìn)行布爾判斷時(shí)就會(huì)使用自定義的邏輯

.. ...

熟悉規(guī)則,并讓自己的代碼適應(yīng)這些規(guī)則,可以幫助我們寫出更地道的代碼,事半功倍的完成工作。下面,讓我們來看一個(gè)有關(guān)適應(yīng)規(guī)則的故事。

案例:從兩份旅游數(shù)據(jù)中獲取人員名單

某日,在一個(gè)主打新西蘭出境游的旅游公司里,商務(wù)同事突然興沖沖的跑過來找到我,說他從某合作伙伴那里,要到了兩份重要的數(shù)據(jù):

所有去過“泰國(guó)普吉島”的人員及聯(lián)系方式

所有去過“新西蘭”的人員及聯(lián)系方式

數(shù)據(jù)采用了 JSON 格式,如下所示:

# 去過普吉島的人員數(shù)據(jù)

users_visited_phuket = [

{"first_name": "Sirena", "last_name": "Gross", "phone_number": "650-568-0388", "date_visited": "2018-03-14"},

{"first_name": "James", "last_name": "Ashcraft", "phone_number": "412-334-4380", "date_visited": "2014-09-16"},

... ...

]

# 去過新西蘭的人員數(shù)據(jù)

users_visited_nz = [

{"first_name": "Justin", "last_name": "Malcom", "phone_number": "267-282-1964", "date_visited": "2011-03-13"},

{"first_name": "Albert", "last_name": "Potter", "phone_number": "702-249-3714", "date_visited": "2013-09-11"},

... ...

]


每份數(shù)據(jù)里面都有著姓、名、手機(jī)號(hào)碼、旅游時(shí)間 四個(gè)字段?;谶@份數(shù)據(jù),商務(wù)同學(xué)提出了一個(gè)(聽上去毫無道理)的假設(shè):“去過普吉島的人,應(yīng)該對(duì)去新西蘭旅游也很有興趣。我們需要從這份數(shù)據(jù)里,找出那些去過普吉島但沒有去過新西蘭的人,針對(duì)性的賣產(chǎn)品給他們。

第一次蠻力嘗試

有了原始數(shù)據(jù)和明確的需求,接下來的問題就是如何寫代碼了。依靠蠻力,我很快就寫出了第一個(gè)方案:

因?yàn)樵紨?shù)據(jù)里沒有“用戶 ID”之類的唯一標(biāo)示,所以我們只能把“姓名和電話號(hào)碼完全相同”作為判斷是不是同一個(gè)人的標(biāo)準(zhǔn)。

find_potential_customers_v1 函數(shù)通過循環(huán)的方式,先遍歷所有去過普吉島的人,然后再遍歷新西蘭的人,如果在新西蘭的記錄中找不到完全匹配的記錄,就把它當(dāng)做“潛在客戶”返回。

這個(gè)函數(shù)雖然可以完成任務(wù),但是相信不用我說你也能發(fā)現(xiàn)。它有著非常嚴(yán)重的性能問題。對(duì)于每一條去過普吉島的記錄,我們都需要遍歷所有新西蘭訪問記錄,嘗試找到匹配。整個(gè)算法的時(shí)間復(fù)雜度是可怕的 O(n*m),如果新西蘭的訪問條目數(shù)很多的話,那么執(zhí)行它將耗費(fèi)非常長(zhǎng)的時(shí)間。

為了優(yōu)化內(nèi)層循環(huán)性能,我們需要減少線性查找匹配部分的開銷。

嘗試使用集合優(yōu)化函數(shù)

如果你對(duì) Python 有所了解的話,那么你肯定知道,Python 里的字典和集合對(duì)象都是基于 哈希表(Hash Table): https://en.wikipedia.org/wiki/Hash_table 實(shí)現(xiàn)的。判斷一個(gè)東西是不是在集合里的平均時(shí)間復(fù)雜度是 O(1),非???。

所以,對(duì)于上面的函數(shù),我們可以先嘗試針對(duì)新西蘭訪問記錄初始化一個(gè)集合,之后的查找匹配部分就可以變得很快,函數(shù)整體時(shí)間復(fù)雜度就能變?yōu)?O(n+m)。

讓我們看看新的函數(shù):

使用了集合對(duì)象后,新函數(shù)在速度上相比舊版本有了飛躍性的突破。但是,對(duì)這個(gè)問題的優(yōu)化并不是到此為止,不然文章標(biāo)題就應(yīng)該改成:“如何使用集合提高程序性能” 了。

對(duì)問題的重新思考

讓我們來嘗試重新抽象思考一下問題的本質(zhì)。首先,我們有一份裝了很多東西的容器 A(普吉島訪問記錄),然后給我們另一個(gè)裝了很多東西的容器 B(新西蘭訪問記錄),之后定義相等規(guī)則:“姓名與電話一致”。最后基于這個(gè)相等規(guī)則,求 A 和 B 之間的“差集”。

如果你對(duì) Python 里的集合不是特別熟悉,我就稍微多介紹一點(diǎn)。假如我們擁有兩個(gè)集合 A 和 B,那么我們可以直接使用 A - B 這樣的數(shù)學(xué)運(yùn)算表達(dá)式來計(jì)算二者之間的 差集。

所以,計(jì)算“所有去過普吉島但沒去過新西蘭的人”,其實(shí)就是一次集合的求差值操作。那么要怎么做,才能把我們的問題套入到集合的游戲規(guī)則里去呢?

利用集合的游戲規(guī)則

在 Python 中,如果要把某個(gè)東西裝到集合或字典里,一定要滿足一個(gè)基本條件:“這個(gè)東西必須是可以被哈希(Hashable)的” 。什么是 “Hashable”?

舉個(gè)例子,Python 里面的所有可變對(duì)象,比如字典,就 不是 Hashable 的。當(dāng)你嘗試把字典放入集合中時(shí),會(huì)發(fā)生這樣的錯(cuò)誤:

所以,如果要利用集合解決我們的問題,就首先得定義我們自己的 “Hashable” 對(duì)象:VisitRecord。而要讓一個(gè)自定義對(duì)象變得 Hashable,唯一要做的事情就是定義對(duì)象的 __hash__ 方法。

一個(gè)好的哈希算法,應(yīng)該讓不同對(duì)象之間的值盡可能的唯一,這樣可以最大程度減少“哈希碰撞”: https://en.wikipedia.org/wiki/Collision_(computer_science)發(fā)生的概率,默認(rèn)情況下,所有 Python 對(duì)象的哈希值來自它的內(nèi)存地址。

在這個(gè)問題里,我們需要自定義對(duì)象的 __hash__ 方法,讓它利用 (姓,名,電話)元組作為 VisitRecord 類的哈希值來源。

自定義完 __hash__ 方法后,VisitRecord 實(shí)例就可以正常的被放入集合中了。但這還不夠,為了讓前面提到的求差值算法正常工作,我們還需要實(shí)現(xiàn) __eq__ 特殊方法。

__eq__ 是 Python 在判斷兩個(gè)對(duì)象是否相等時(shí)調(diào)用的特殊方法。默認(rèn)情況下,它只有在自己和另一個(gè)對(duì)象的內(nèi)存地址完全一致時(shí),才會(huì)返回 True。但是在這里,我們復(fù)用了 VisitRecord 對(duì)象的哈希值,當(dāng)二者相等時(shí),就認(rèn)為它們一樣。

完成了恰當(dāng)?shù)臄?shù)據(jù)建模后,之后的求差值運(yùn)算便算是水到渠成了。新版本的函數(shù)只需要一行代碼就能完成操作:

Hint:如果你使用的是 Python 2,那么除了 __eq__ 方法外,你還需要自定義類的 __ne__(判斷不相等時(shí)使用) 方法。

使用 dataclass 簡(jiǎn)化代碼

故事到這里并沒有結(jié)束。在上面的代碼里,我們手動(dòng)定義了自己的 數(shù)據(jù)類 VisitRecord,實(shí)現(xiàn)了 __init__、__eq__ 等初始化方法。但其實(shí)還有更簡(jiǎn)單的做法。

因?yàn)槎x數(shù)據(jù)類這種需求在 Python 中實(shí)在太常見了,所以在 3.7 版本中,標(biāo)準(zhǔn)庫(kù)中新增了 dataclasses 模塊,專門幫你簡(jiǎn)化這類工作。

如果使用 dataclasses 提供的特性,我們的代碼可以最終簡(jiǎn)化成下面這樣:

不用干任何臟活累活,只要不到十行代碼就完成了工作。

案例總結(jié)

問題解決以后,讓我們?cè)僮鲆稽c(diǎn)小小的總結(jié)。在處理這個(gè)問題時(shí),我們一共使用了三種方案:

使用普通的兩層循環(huán)篩選符合規(guī)則的結(jié)果集

利用哈希表結(jié)構(gòu)(set 對(duì)象)創(chuàng)建索引,提升處理效率

將數(shù)據(jù)轉(zhuǎn)換為自定義對(duì)象,利用規(guī)則,直接使用集合運(yùn)算

為什么第三種方式會(huì)比前面兩種好呢?

首先,第一個(gè)方案的性能問題過于明顯,所以很快就會(huì)被放棄。那么第二個(gè)方案呢?仔細(xì)想想看,方案二其實(shí)并沒有什么明顯的缺點(diǎn)。甚至和第三個(gè)方案相比,因?yàn)樯倭俗远x對(duì)象的過程,它在性能與內(nèi)存占用上,甚至有可能會(huì)微微強(qiáng)于后者。

但請(qǐng)?jiān)偎伎家幌?,如果你把方案二的代碼換成另外一種語言,比如 Java,它是不是基本可以做到 1:1 的完全翻譯?換句話說,它雖然效率高、代碼直接,但是它沒有完全利用好 Python 世界提供的規(guī)則,最大化的從中受益。

如果要具體化這個(gè)問題里的“規(guī)則”,那就是 “Python 擁有內(nèi)置結(jié)構(gòu)集合,集合之間可以進(jìn)行差值等四則運(yùn)算” 這個(gè)事實(shí)本身。匹配規(guī)則后編寫的方案三代碼擁有下面這些優(yōu)勢(shì):

為數(shù)據(jù)建模后,可以更方便的定義其他方法

如果需求變更,做反向差值運(yùn)算、求交集運(yùn)算都很簡(jiǎn)單

理解集合與 dataclasses 邏輯后,代碼遠(yuǎn)比其他版本更簡(jiǎn)潔清晰

如果要修改相等規(guī)則,比如“只擁有相同姓的記錄就算作一樣”,只需要繼承VisitRecord 覆蓋 __eq__ 方法即可

其他規(guī)則如何影響我們

在前面,我們花了很大的篇幅講了如何利用“集合的規(guī)則”來編寫事半功倍的代碼。除此之外,Python 世界中還有著很多其他規(guī)則。如果能熟練掌握這些規(guī)則,就可以設(shè)計(jì)出符合 Python 慣例的 API,讓代碼更簡(jiǎn)潔精煉。

下面是兩個(gè)具體的例子。

使用 __format__ 做對(duì)象字符串格式化

如果你的自定義對(duì)象需要定義多種字符串表示方式,就像下面這樣:

那么除了增加這種 get_xxx_display() 額外方法外,你還可以嘗試自定義 Student 類的 __format__ 方法,因?yàn)槟遣攀菍?duì)象變?yōu)樽址臉?biāo)準(zhǔn)規(guī)則。

使用 __getitem__ 定義對(duì)象切片操作

如果你要設(shè)計(jì)某個(gè)可以裝東西的容器類型,那么你很可能會(huì)為它定義“是否為空”、“獲取第 N 個(gè)對(duì)象”等方法:

但是,這樣并非最好的做法。因?yàn)?Python 已經(jīng)為我們提供了一套對(duì)象規(guī)則,所以我們不需要像寫其他語言的 OO(面向?qū)ο螅?代碼那樣去自己定義額外方法。我們有更好的選擇:

新的寫法相比舊代碼,更能適配進(jìn) Python 世界的規(guī)則,API 也更為簡(jiǎn)潔。

關(guān)于如何適配規(guī)則、寫出更好的 Python 代碼。Raymond Hettinger 在 PyCon 2015 上有過一次非常精彩的演講 “Beyond PEP8 - Best practices for beautiful intelligible code”: https://www.youtube.com/watch?v=wf-BqAjZb8M。這次演講長(zhǎng)期排在我個(gè)人的 “PyCon 視頻 TOP5” 名單上,如果你還沒有看過,我強(qiáng)烈建議你現(xiàn)在就去看一遍 :)

Hint:更全面的 Python 對(duì)象模型規(guī)則可以在 官方文檔:https://docs.python.org/3/reference/datamodel.html 找到,有點(diǎn)難讀,但值得一讀。

總結(jié)

Python 世界有著一套非常復(fù)雜的規(guī)則,這些規(guī)則的涵蓋范圍包括“對(duì)象與對(duì)象是否相等“、”對(duì)象與對(duì)象誰大誰小”等等。它們大部分都需要通過重新定義“雙下劃線方法 __xxx__” 去實(shí)現(xiàn)。

如果熟悉這些規(guī)則,并在日常編碼中活用它們,有助于我們更高效的解決問題、設(shè)計(jì)出更符合 Python 哲學(xué)的 API。下面是本文的一些要點(diǎn)總結(jié):

永遠(yuǎn)記得對(duì)原始需求做抽象分析,比如問題是否能用集合求差集解決

如果要把對(duì)象放入集合,需要自定義對(duì)象的 __hash__ 與 __eq__ 方法

__hash__ 方法決定性能(碰撞出現(xiàn)概率),__eq__ 決定對(duì)象間相等邏輯

使用 dataclasses 模塊可以讓你少寫很多代碼

使用 __format__ 方法替代自己定義的字符串格式化方法

在容器類對(duì)象上使用 __len__、__getitem__ 方法,而不是自己實(shí)現(xiàn)

藍(lán)鯨智云

本文由騰訊藍(lán)鯨智云編輯發(fā)布,騰訊藍(lán)鯨智云(簡(jiǎn)稱藍(lán)鯨)軟件體系是一套基于PaaS的技術(shù)解決方案,致力于打造行業(yè)領(lǐng)先的一站式自動(dòng)化運(yùn)維平臺(tái)。目前已經(jīng)推出社區(qū)版、企業(yè)版,歡迎體驗(yàn)。

官網(wǎng):https://bk.tencent.com/

下載鏈接:https://bk.tencent.com/download/

社區(qū):https://bk.tencent.com/s-mart/community/question


Python 工匠:做一個(gè)精通規(guī)則的玩家的評(píng)論 (共 條)

分享到微博請(qǐng)遵守國(guó)家法律
古丈县| 兰坪| 盐亭县| 姚安县| 玉环县| 利川市| 大宁县| 华坪县| 泌阳县| 宜兴市| 汉中市| 沭阳县| 印江| 清远市| 同江市| 巴楚县| 莱阳市| 永仁县| 玉树县| 勐海县| 江陵县| 新绛县| 普宁市| 巴林右旗| 镇雄县| 衢州市| 赫章县| 青冈县| 永寿县| 敦化市| 青川县| 钟山县| 教育| 延寿县| 光山县| 永善县| 宁远县| 韩城市| 宁乡县| 绿春县| 历史|