由一個簡單的Python合并字典問題引發(fā)的思考,如何優(yōu)化我們的代碼?
關(guān)注公眾號《云爬蟲技術(shù)研究筆記》,獲取更多干貨~
號主介紹
多年反爬蟲破解經(jīng)驗(yàn),AKA“逆向小學(xué)生”,沉迷數(shù)據(jù)分析和黑客增長不能自拔,虛名有CSDN博客專家和華為云享專家。
今天我們的題目是《由一個簡單的Python合并字典問題引發(fā)的思考,如何優(yōu)化我們的代碼?》,為什么會有這個話題呢?起因是今天和一位剛剛面試完P(guān)ython開發(fā)崗位的朋友交流,這個問題也是他在面試中遇到的問題:
怎么用一個簡單的表達(dá)式合并Python中的兩個Dict?
這個問題雖然是一道很簡單的問題,并且解題的思路也有很多種。不過問題雖小,但是我們也借此分析一下更深層次的東西,關(guān)于代碼如何優(yōu)化,優(yōu)化思路等等。
首先我們簡單的思考一下,Python中合并兩個Dict有哪些方法?我們分別舉Python3和Python2的例子。
假設(shè)我們現(xiàn)在有DictXX和DictYY,我們想要合并它們得到新的DictZZ,我們會這么做:
在Python 3.5或更高版本中:
z?=?{**x,?**y}
在Python 2(或3.4或更低版本)中,編寫一個函數(shù):
def?merge_two_dicts(x,?y): ????z?=?x.copy()???#?start?with?x's?keys?and?values ????z.update(y)????#?modifies?z?with?y's?keys?and?values?&?returns?None ????return?z z?=?merge_two_dicts(x,?y)
假設(shè)我們有兩個字典,并且想要將它們合并為新字典而不更改原始字典:
x?=?{'a':?1,?'b':?2} y?=?{'b':?3,?'c':?4}
理想的結(jié)果是獲得一個z是合并后的新字典,第二個Dict的值覆蓋第一個字典Dict的值。
>>>?z {'a':?1,?'b':?3,?'c':?4}
在PEP 448中提出并從Python 3.5開始可用的新語法是:
z?=?{**x,?**y}
它只需要一個非常簡潔的表達(dá)式就可以完成,另外,我們也可以使用解包來進(jìn)行操作:
z?=?{**x,?'foo':?1,?'bar':?2,?**y}
結(jié)果如下:
>>>?z {'a':?1,?'b':?3,?'foo':?1,?'bar':?2,?'c':?4}
它現(xiàn)在正式的在3.5的發(fā)布時間表中實(shí)現(xiàn),PEP 478,并且已進(jìn)入Python 3.5的新功能文檔。
我們大致看一下這個新功能的使用方式
這個功能允許我們在同一個表達(dá)式中使用多個解包表達(dá)式,能夠很方便的合并迭代器和普通的列表,而不需要將迭代器先轉(zhuǎn)化成列表再進(jìn)行合并。
但是,由于許多組織仍在使用Python 2,因此我們可能希望以向后兼容的方式進(jìn)行操作。在Python 2和Python 3.0-3.4中可用的經(jīng)典Pythonic方法是分兩個步驟完成的:
z?=?x.copy() z.update(y)?#?which?returns?None?since?it?mutates?z
這種方法中,我們拷貝x生成新的對象z,再使用dict的update的方法合并兩個dict。
如果我們尚未使用Python 3.5,或者需要編寫向后兼容的代碼,并且希望在單個表達(dá)式中使用它,則最有效的方法是將其放入函數(shù)中:
def?merge_two_dicts(x,?y): ????"""Given?two?dicts,?merge?them?into?a?new?dict?as?a?shallow?copy.""" ????z?=?x.copy() ????z.update(y)????return?z
然后我們需要這樣使用函數(shù):
z?=?merge_two_dicts(x,?y)
您還可以創(chuàng)建一個合并多個dict的函數(shù),并且我們可以指定任意數(shù)量的dict:
def?merge_dicts(*dict_args): ????""" ????Given?any?number?of?dicts,?shallow?copy?and?merge?into?a?new?dict, ????precedence?goes?to?key?value?pairs?in?latter?dicts. ????""" ????result?=?{}????for?dictionary?in?dict_args: ????????result.update(dictionary)????return?result
此函數(shù)將在Python 2和3中適用于所有字典。我們可以這樣使用:
z?=?merge_dicts(a,?b,?c,?d,?e,?f,?g)
不過注意的是:越往后的dict的鍵值優(yōu)先度越高,會覆蓋前面的鍵值。
在Python 2中,我們還可以這么操作:
z?=?dict(x.items()?+?y.items())
在Python 2中,我們使用.items()會得到list,也就是我們將會在內(nèi)存中創(chuàng)建兩個列表,然后在內(nèi)存中創(chuàng)建第三個列表,其長度等于前兩個字典的長度,最后丟棄所有三個列表以創(chuàng)建字典,就是我們需要的Dict。
但是注意,我們決不能在Python 3中這么使用,在Python 3中,這會失敗失敗是因?yàn)槲覀兪菍蓚€dict_items對象而不是兩個列表加在一起。
>>>?c?=?dict(a.items()?+?b.items()) Traceback?(most?recent?call?last): ??File?"
當(dāng)然,我們真的想要實(shí)現(xiàn)的話,我們也可以強(qiáng)制轉(zhuǎn)換,將它們明確創(chuàng)建為列表,例如z = dict(list(x.items()) + list(y.items())),但是這反而浪費(fèi)了資源和計算能力。
類似地,當(dāng)值是不可散列的對象(例如列表)時,items()在Python 3(viewitems()在Python 2.7中)進(jìn)行聯(lián)合也將失敗。即使您的值是可哈希的,由于集合在語義上是無序的,因此關(guān)于優(yōu)先級的行為是不確定的。所以不要這樣做:
>>>?c?=?dict(a.items()?|?b.items())
我們演示一下值不可散列時會發(fā)生的情況:
>>>?x?=?{'a':?[]}>>>?y?=?{'b':?[]}>>>?dict(x.items()?|?y.items()) Traceback?(most?recent?call?last): ??File?"
這是一個示例,其中y應(yīng)該優(yōu)先,但是由于集合的任意順序,保留了x的值:
>>>?x?=?{'a':?2}>>>?y?=?{'a':?1}>>>?x.items()?|?y.items() {('a',?1),?('a',?2)} >>>?dict(x.items()?|?y.items()) {'a':?2}
另外一種我們不應(yīng)該使用的另一種技巧:
z?=?dict(x,?**y)
這使用了dict構(gòu)造函數(shù),并且非常快速且具有內(nèi)存效率(甚至比我們的兩步過程略高),但是除非我們確切地知道里面正在發(fā)生什么(也就是說,第二個dict作為關(guān)鍵字參數(shù)傳遞給dict,構(gòu)造函數(shù))我們才能使用,要不然這個表達(dá)式很難閱讀,有時我們并不能很快的理解這算什么用法,因此不算Pythonic。
由于這種情況的存在,我們看看在django中修復(fù)的用法示例。
字典旨在獲取可散列的鍵(例如,frozenset或tuple),但是當(dāng)鍵不是字符串時,此方法在Python 3中失敗。
>>>?c?=?dict(a,?**b) Traceback?(most?recent?call?last): ??File?"
在郵件列表中,大佬Guido van Rossum寫道:
我宣布dict({},**?{1:3})是非法的使用方式,因?yàn)檫@是對**機(jī)制的濫用。 顯然dict(x,**?y)和直接調(diào)用x.update(y)并返回x這種“酷”的操作很類似。 但是我個人覺得它比“酷”的操作更低俗。
不過根據(jù)我的理解(以及對大佬的話的理解),dict(**y)命令的預(yù)期用途是為了創(chuàng)建可讀性強(qiáng)的字典,例如:
dict(a=1,?b=10,?c=11)
用來代替
{'a':?1,?'b':?10,?'c':?11}
在這個地方使用**運(yùn)算符也不會濫用該機(jī)制,我們使用**正是為了將dict作為關(guān)鍵字傳遞而設(shè)計的。
這些方法的性能較差,但是它們將提供正確的行為。它們的性能將不及copy和update或新的解包方式,因?yàn)樗鼈冊诟叩某橄蠹墑e上遍歷每個鍵值對,但它們確實(shí)遵循優(yōu)先級的順序(后者決定了優(yōu)先級)
我們可以在使用生成式來做:
{k:?v?for?d?in?dicts?for?k,?v?in?d.items()}?#?iteritems?in?Python?2.7
或在python 2.6中(也許在引入生成器表達(dá)式時早在2.4中):
dict((k,?v)?for?d?in?dicts?for?k,?v?in?d.items())
itertools.chain?迭代器的騷操作:
import?itertools z?=?dict(itertools.chain(x.iteritems(),?y.iteritems()))
ChainMap的騷操作:
>>>?from?collections?import?ChainMap>>>?x?=?{'a':1,?'b':?2}>>>?y?=?{'b':10,?'c':?11}>>>?z?=?ChainMap({},?y,?x)>>>?for?k,?v?in?z.items(): ????????print(k,?'-->',?v) a?-->?1b?-->?10c?-->?11
我將僅對已知行為正確的用法進(jìn)行性能分析。
import?timeit
在Ubuntu 18上完成以下操作
在Python 2.7(系統(tǒng)Python)中:
>>>?min(timeit.repeat(lambda:?merge_two_dicts(x,?y))) 0.5726828575134277>>>?min(timeit.repeat(lambda:?{k:?v?for?d?in?(x,?y)?for?k,?v?in?d.items()}?)) 1.163769006729126>>>?min(timeit.repeat(lambda:?dict(itertools.chain(x.iteritems(),?y.iteritems())))) 1.1614501476287842>>>?min(timeit.repeat(lambda:?dict((k,?v)?for?d?in?(x,?y)?for?k,?v?in?d.items()))) 2.2345519065856934
在Python 3.5中:
>>>?min(timeit.repeat(lambda:?{**x,?**y}))0.4094954460160807>>>?min(timeit.repeat(lambda:?merge_two_dicts(x,?y)))0.7881555100320838>>>?min(timeit.repeat(lambda:?{k:?v?for?d?in?(x,?y)?for?k,?v?in?d.items()}?))1.4525277839857154>>>?min(timeit.repeat(lambda:?dict(itertools.chain(x.items(),?y.items()))))2.3143140770262107>>>?min(timeit.repeat(lambda:?dict((k,?v)?for?d?in?(x,?y)?for?k,?v?in?d.items())))3.2069112799945287
經(jīng)過我們之前的一系列分析和實(shí)驗(yàn),我們可以得到這個問題的結(jié)論
Python 2中我們就采用copy加上update的方案
Python 3中我們就采用多重解包的方案
不過對比以上兩種,顯然多重解包更快而且更簡潔,針對大家不熟悉Python 3可以參考我之前的一篇文章Python2壽命只剩一個月啦!還不快趕緊學(xué)起Python3酷炫到爆的新特性!,可以幫助大家快速的切換成Python 3開發(fā),不過注意的是Python 3高版本和Python 2.7差別也是比較大,因此大家要是涉及線上業(yè)務(wù)的切換,請謹(jǐn)慎注意!
最后我們來談?wù)剝?yōu)化代碼的問題,從這個問題入手,我們可以總結(jié)出優(yōu)化代碼的思路:
我們分析出有哪些解決方案?
哪些解決方案是有效的?
這些有效的方案怎么做對比?
最佳的方案需要我們做出哪些犧牲?
python
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時內(nèi)刪除侵權(quán)內(nèi)容。