讓你的代碼更“地道”:提高Python編碼水平的小技巧
本文轉(zhuǎn)載自公眾號(hào)“讀芯術(shù)”(ID:AI_Discovery)
各個(gè)學(xué)科領(lǐng)域和行業(yè)都在經(jīng)歷一場“Python熱”。在觀察生物醫(yī)學(xué)領(lǐng)域中Python編程的運(yùn)用情況之后,筆者意識(shí)到相當(dāng)多的Python程序員,都有不同的編程使用經(jīng)驗(yàn),如Matlab、C語言、C++、Java、JavaScript和Swift,也有一些之前沒有編程經(jīng)驗(yàn)的人。
Python成了程序員的“外語”,他們可能沒經(jīng)過系統(tǒng)的Python編碼培訓(xùn),也可能并不知道Python開發(fā)的慣用方法。
雖然程序員依然可以通過不同的方式實(shí)現(xiàn)同樣的功能,編寫出優(yōu)秀的代碼,只要代碼能夠滿足預(yù)期目的就OK。編寫非慣用Python程序也沒有問題。但就像我們不斷練習(xí)英文的口音一樣,也有一些人也想讓自己的Python代碼變得更地道。
本文中,我將分享自己在過去幾年中積累的一些習(xí)慣用法,希望對(duì)提高你的Python編碼水平有所幫助。
1. 分割序列
常見的序列類型有列表、元組和字符串。通過分割另一個(gè)序列,可以創(chuàng)建一個(gè)新序列。以下功能用列表作為示例,不過它們也可以用于元組、字符串和字節(jié)等其他序列類型。
- >>> a = [0, 2, 4, 6, 8,10, 12, 14, 16, 18, 20]
- >>> # Using a range, [start, end)
- >>> a[1:3]
- [2, 4]
- >>> # Using a range with a step
- >>> a[1:9:2]
- [2, 6, 10, 14]
- >>> # Leave out the start = an implicit start of 0
- >>> a[:5]
- [0, 2, 4, 6, 8]
- >>> # Leave out the stop = an implict end to the very last item
- >>> a[9:]
- [18, 20]
- >>> # Entire list
- >>> a[:]
- [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
2. 使用反向索引訪問序列中的元素
如果想在序列的末尾訪問一些元素,那么反向計(jì)數(shù)要容易得多。在Python序列中,最后一個(gè)元素的索引是-1,倒數(shù)第二個(gè)元素的索引是-2,以此類推。
- >>> a = 'Hello World!'
- >>> # instead of using a[len(a)-1]
- >>> a[-1]
- '!'
- >>> # in combination with slicing
- >>> a[-5:-1]
- 'orld'
3. 多重賦值
在給幾個(gè)變量賦值時(shí),可以使用多重賦值。通過同樣的習(xí)慣用法,可以交換同一列表中的兩個(gè)變量或兩個(gè)元素。這一特征與之后要介紹的元組解包密切相關(guān)。
- >>> # instead of doing a =8; b = 5
- >>> a, b = 8, 5
- >>> print(f'a is {a}; b is ')
- a is 8; b is 5
- >>> # Swap two variables
- >>> a, bb = b, a
- >>> print(f'a is {a}; b is ')
- a is 5; b is 8
- >>> # Swap the first and last elements in a list
- >>> numbers = [1, 2, 3, 4, 5]
- >>> numbers[0], numbers[-1] = numbers[-1], numbers[0]
- >>> numbers
- [5, 2, 3, 4, 1]
4. 顛倒序列
有時(shí)需要顛倒序列。雖然可以用for循環(huán)語句來實(shí)現(xiàn),但是還有一種更簡單直接的方法。與上述情況類似,當(dāng)某個(gè)功能可用于某個(gè)序列時(shí),通常意味著字符串、元組和列表也都支持這個(gè)功能。
- >>> a = (1, 2, 3, 4, 5)
- >>> a[::-1]
- (5, 4, 3, 2, 1)
- >>> b = 'start'
- >>> b[::-1]
- 'trats'
5. 檢查序列是否為空
只有序列不為空時(shí),列表、元組等操作才行得通,因此需要在操作之前檢查序列是否為空。
- >>> empty_list = [(), '',[], {}, set()]
- >>> for item in empty_list:
- ... if not item:
- ... print(f'Do something with the{type(item)}')
- ...
- Do something with the <class 'tuple'>
- Do something with the <class 'str'>
- Do something with the <class 'list'>
- Do something with the <class 'dict'>
- Do something with the <class 'set'>
為此,可以用not關(guān)鍵字來否定序列(例如not[]),只要序列不為空,其值就為True。此外,還可以對(duì)另外兩種常見的數(shù)據(jù)類型dict和set執(zhí)行同樣的操作。
6. 集合推導(dǎo)式
集合推導(dǎo)式的用法與上述列表解析式的用法類似。不同之處在于集合推導(dǎo)式用的是花括號(hào)而不是方括號(hào)。并且,通過定義set 數(shù)據(jù)類型,可以刪除重復(fù)的元素。
7. 字典生成式
除了列表解析式和集合推導(dǎo)式外,解析式特征還可用于字典數(shù)據(jù)類型的創(chuàng)建。dict由鍵值對(duì)組成,因此字典生成式包含指定鍵和值,二者之間用冒號(hào)隔開。
- >>> a = [1, 2, 3, 4, 5]
- >>> {x: x*x for x in a}
- {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
8. 生成器表達(dá)式
Python中的生成器是創(chuàng)建迭代器的一種簡便方法。因?yàn)樯善魇?ldquo;惰性的”(也就是說,只有當(dāng)發(fā)出請(qǐng)求時(shí)才能生成需要的項(xiàng))。生成器非常節(jié)省內(nèi)存。
創(chuàng)建生成器的一種特殊方法稱為生成器表達(dá)式。除了用圓括號(hào)而非方括號(hào)這一點(diǎn)外,生成器表達(dá)式在語法上與列表解析式類似。
下面的例子中,當(dāng)生成器直接用于迭代函數(shù)時(shí),圓括號(hào)可有可無。
- >>> sum(x**2 for x inrange(100))
- 328350
- >>> max((x*x for x in range(100)))
- 9801
9. 列表解析式
Python中一個(gè)有用的特征是列表解析式。通過列表解析式,可以很方便地構(gòu)造一個(gè)列表。列表解析式的一般格式為[some_expression for element initerable if some_condition]。
- >>> a = [1, 2, 3, 4, 5]
- >>> [x*2 for x in a]
- [2, 4, 6, 8, 10]
- >>> [x*3 for x in a if x%2 == 1]
- [3, 9, 15]
10. 解包元組
元組是Python中十分常見的數(shù)據(jù)結(jié)構(gòu)。它們是一組組相關(guān)的值。元組的常見用法包括訪問自身元素。雖然可以使用索引訪問這些元素,但是解包是一種更為簡便的方法。
與解包的用法有關(guān),可以用下劃線來表示不需要的元素,用星號(hào)給已命名元素之外的其他元素賦值。
- >>> items = (0, 'b','one', 10, 11, 'zero')
- >>> a, b, c, d, e, f = items
- >>> print(f)
- zero
- >>> a, *b, c = items
- >>> print(b)
- ['b', 'one', 10, 11]
- >>> *_, a, b = items
- >>> print(a)
- 11
11. 在for循環(huán)語句中使用Reversed()函數(shù)
reversed()函數(shù)通常用在for循環(huán)語句中,是一種以與原始可迭代對(duì)象相反的順序創(chuàng)建迭代器的方法。
- >>> tasks = ['laundry','picking up kids', 'gardening', 'cooking']
- >>> for task in reversed(tasks):
- ... print(task)
- ...
- cooking
- gardening
- picking up kids
- laundry
12. Zip()壓縮函數(shù)
zip()函數(shù)在一對(duì)一匹配連接多個(gè)迭代器方面十分有用。如果某些迭代器超過最短的迭代器,就會(huì)被截?cái)?。zip()函數(shù)返回一個(gè)迭代器,因此在迭代中經(jīng)常使用到。還可以用zip()函數(shù)解壓縮帶星號(hào)的迭代器,并將未壓縮的項(xiàng)賦值給變量。
- >>> students = ('John','Mary', 'Mike')
- >>> ages = (15, 17, 16)
- >>> scores = (90, 88, 82, 17, 14)
- >>> for student, age, score in zip(students, ages, scores):
- ... print(f'{student}, age: {age},score: {score}')
- ...
- John, age: 15, score: 90
- Mary, age: 17, score: 88
- Mike, age: 16, score: 82
- >>> zipzipped = zip(students, ages, scores)
- >>> a, b, c = zip(*zipped)
- >>> print(b)
- (15, 17, 16)
13. 用Lambdas排序
lambdas表達(dá)式為匿名函數(shù),可以用單行表達(dá)式接收多個(gè)參數(shù)。lambdas的一個(gè)常見用法是將其設(shè)置為內(nèi)置sorted()函數(shù)中的key參數(shù)。
除此之外,lambdas還經(jīng)常用于max(),map()等函數(shù)中。在這些函數(shù)中,可以用單行表達(dá)式來替換使用def關(guān)鍵字的常規(guī)函數(shù)。
- >>> students = [{'name':'John', 'score': 98}, {'name': 'Mike', 'score': 94}, {'name': 'Jennifer','score': 99}]
- >>> sorted(students, key=lambda x: x['score'])
- [{'name': 'Mike', 'score': 94}, {'name': 'John', 'score': 98}, {'name':'Jennifer', 'score': 99}]
14. 速記條件賦值
該特征基本上是個(gè)語法糖。在根據(jù)特定條件為變量賦值時(shí),可以用以下通用速記賦值:y = x if condition_met elseanother_x。
- >>> some_condition = True
- >>> # the expanded format
- >>> if some_condition:
- ... x = 5
- ... else:
- ... x = 3
- >>> print(f'x is {x}')
- x is 5
- >>> # the shorthand way
- >>> x = 5 if some_condition else 3
- >>> print(f'x is {x}')
- x is 5
15. 在for循環(huán)語句中使用Enumerate()枚舉函數(shù)
用enumerate()函數(shù)獲取可迭代對(duì)象來創(chuàng)建迭代器。此外,enumerate()函數(shù)還可以跟蹤迭代的次數(shù)??梢噪S意設(shè)置計(jì)數(shù)初始值。默認(rèn)的計(jì)數(shù)初始值為0。
- >>> students = ('John','Mary', 'Mike')
- >>> for i, student in enumerate(students):
- ... print(f'Iteration: {i}, Student:{student}')
- ...
- Iteration: 0, Student: John
- Iteration: 1, Student: Mary
- Iteration: 2, Student: Mike
- >>> for i, student in enumerate(students, 35001):
- ... print(f'Student Name: {student},Student ID #: {i}')
- ...
- Student Name: John, Student ID #: 35001
- Student Name: Mary, Student ID #: 35002
- Student Name: Mike, Student ID #: 35003
16. 用Get()方法檢索字典中的值
通常情況下,可以在方括號(hào)中指定鍵來檢索鍵的值。但是,當(dāng)鍵不在字典中時(shí),就可能出錯(cuò)。當(dāng)然,也可以用try/except異常處理機(jī)制來解決這個(gè)問題。不過,當(dāng)鍵不在字典中時(shí),還可以通過get()方法設(shè)置默認(rèn)值。
- >>> number_dict = {0:'zero', 1: 'one', 2: 'two', 3: 'three'}
- >>> number_dict[5]
- Traceback (most recent call last):
- File "<stdin>", line 1,in <module>
- KeyError: 5
- >>> number_dict.get(5, 'five')
- 'five'
17. 獲取字典中最大值對(duì)應(yīng)的鍵
有時(shí)需要在字典中找出最大值對(duì)應(yīng)的鍵。首先,在所有值列表中找到最大值的索引,然后從另一個(gè)存儲(chǔ)所有鍵的列表中找到對(duì)應(yīng)的鍵?;蛘?,也可以用一種更簡單的方法,就是在max()函數(shù)中指定key參數(shù)。
簡便起見,不考慮最大值可能重復(fù)的情況。同樣地,還可以用min()函數(shù)查找最小值對(duì)應(yīng)的鍵。
- >>> model_scores ={'model_a': 100, 'model_z': 198, 'model_t': 150}
- >>> # workaround
- >>> keys, values = list(model_scores.keys()),list(model_scores.values())
- >>> keys[values.index(max(values))]
- 'model_z'
- >>> # one-line
- >>> max(model_scores, key=model_scores.get)
- 'model_z'
18. 用Print()函數(shù)進(jìn)行調(diào)試
對(duì)于較小的項(xiàng)目,可以用print()函數(shù)進(jìn)行調(diào)試。此函數(shù)也經(jīng)常用在教學(xué)中。print()函數(shù)中,經(jīng)常會(huì)用到一些技巧。第一個(gè)是結(jié)束字符串而不是默認(rèn)的換行符;第二個(gè)是使用字符串f-string,創(chuàng)建包含一些表達(dá)式的字符串。
- >>> for i in range(5):
- ... print(i, end=', ' if i < 4else '\n')
- ...
- 0, 1, 2, 3, 4
- >>> for i in range(5):
- ... print(f'{i} & {i*i}', end=',' if i < 4 else '\n')
- ...
- 0 & 0, 1 & 1, 2 & 4, 3 & 9, 4 & 16
19. 集合元素存在性測試
有時(shí),在對(duì)集合或匹配項(xiàng)進(jìn)行操作之前,需要測試集合中是否存在某個(gè)元素。慣用的方法是使用關(guān)鍵字in。
- >>> a = ('one', 'two','three', 'four', 'five')
- >>> if 'one' in a:
- ... print('The tuple contains one.')
- ...
- The tuple contains one.
- >>> b = {0: 'zero', 1: 'one', 2: 'two', 3: 'three'}
- >>> if 2 in b.keys():
- ... print('The dict has the key of2.')
- ...
- The dict has the key of 2.
20. 海象運(yùn)算符
海象運(yùn)算符(:=)是Python 3.8版本中的新功能。它不過是賦值表達(dá)式(給表達(dá)式中的變量賦值)的另一個(gè)名稱。
通常,當(dāng)表達(dá)式使用變量時(shí),變量聲明須提前。使用海象運(yùn)算符,變量賦值可以包含在表達(dá)式中,并且可以立即使用該變量。
- >>> a = ['j', 'a', 'k','d', 'c']
- >>> if (n := len(a))%2 == 1:
- ... print(f'The number of letters is{n}, which is odd.')
- ...
- The number of letters is 5, which is odd.
21. 分割字符串
在處理字符串時(shí),通常要將字符串分隔成單詞列表。這種情況下,可以使用split()函數(shù)的分隔符,并且可選最大分隔。相關(guān)的函數(shù)有rsplit()函數(shù),和split()函數(shù)功能類似,只是在設(shè)置時(shí)從右側(cè)開始分割,以滿足最大分割要求。
- >>> sentence = 'this is, apython, tutorial, about, idioms.'
- >>> sentence.split(', ')
- ['this is', 'a python', 'tutorial', 'about', 'idioms.']
- >>> sentence.split(', ', 2)
- ['this is', 'a python', 'tutorial, about, idioms.']
- >>> sentence.rsplit(', ')
- ['this is', 'a python', 'tutorial', 'about', 'idioms.']
- >>> sentence.rsplit(', ', 2)
- ['this is, a python, tutorial', 'about', 'idioms.']
22. Map()映射函數(shù)
map()函數(shù)是個(gè)高階函數(shù)(即使用函數(shù)作為參數(shù)或返回一個(gè)值作為其輸出的函數(shù))。其通用格式為map(function, iterables)。map()函數(shù)將可迭代對(duì)象作為參數(shù)并返回一個(gè)map 對(duì)象,map 對(duì)象又是一個(gè)迭代器??傻鷮?duì)象的數(shù)量應(yīng)與函數(shù)所需的參數(shù)數(shù)量匹配。
以下示例中,內(nèi)置函數(shù)pow()需要兩個(gè)參數(shù)。當(dāng)然,也可以使用自定義函數(shù)。順便說明一下,在使用map()函數(shù)創(chuàng)建列表時(shí),應(yīng)該可以使用列表解析式達(dá)到相同的效果。
- >>> numbers = (1, 2, 4, 6)
- >>> indices = (2, 1, 0.5, 2)
- >>> # use map()
- >>> list(map(pow, numbers, indices))
- [1, 2, 2.0, 36]
- >>> # list comprehensions
- >>> [pow(x, y) for x, y in zip(numbers, indices)]
- [1, 2, 2.0, 36]
23. Filter()過濾函數(shù)
filter()函數(shù)使用指定的函數(shù)或lambda函數(shù)過濾序列。該函數(shù)返回一個(gè)filter對(duì)象,filter對(duì)象是個(gè)迭代器??偟膩碚f,該函數(shù)的用法與map()函數(shù)非常相似。
- >>> def good_word(x: str):
- ... has_vowels = not set('aeiou').isdisjoint(x.lower())
- ... long_enough = len(x) > 7
- ... good_start =x.lower().startswith('pre')
- ... return has_vowels &long_enough & good_start
- ...
- >>> words = ['Good', 'Presentation', 'preschool', 'prefix']
- >>> list(filter(good_word, words))
- ['Presentation', 'preschool']
24. 連接可迭代對(duì)象中的字符串
在處理字符串時(shí),有時(shí)需要通過連接列表、元組等可迭代對(duì)象內(nèi)的一系列字符串來創(chuàng)建單個(gè)字符串。這種情況下,可以用所需分隔符調(diào)用的join()函數(shù)。
- >>> words = ('Hello','Python', 'Programmers')
- >>> '!'.join(words)
- 'Hello!Python!Programmers'
- >>> words_dict = {0: 'zero', 1: 'one', 2: 'two', 3: 'three'}
- >>> '&'.join(words_dict.values())
- 'zero&one&two&three'
25. 查找列表中最常見的元素
在用列表記錄一些具有重復(fù)元素的內(nèi)容時(shí),例如跟蹤一系列游戲的獲勝者,這與查找哪位游戲玩家贏得的次數(shù)最多有關(guān)??梢酝ㄟ^ max()函數(shù)指定key參數(shù),對(duì)集合中元素的計(jì)數(shù)來找出最大值。
- >>> winnings = ['John','Billy', 'Billy', 'Sam', 'Billy', 'John']
- >>> max(set(winnings), key = winnings.count)
- 'Billy'
26. 檢驗(yàn)對(duì)象類型
檢驗(yàn)對(duì)象類型是Python內(nèi)省功能的一部分。有時(shí),在應(yīng)用對(duì)應(yīng)函數(shù)之前,需要知道對(duì)象是否為某種類型。于是,可以用type()函數(shù)或isinstance()函數(shù),后者更為靈活,可以進(jìn)行一對(duì)多的檢驗(yàn)。
- >>> def check_type(number):
- ... if type(number) == int:
- ... print('do something with anint')
- ... if isinstance(number, (int,float)):
- ... print('do something with anint or float')
- ...
- >>> check_type(5)
- do something with an int
- do something with an int or float
- >>> check_type(4.2)
- do something with an int or float
27. Any() 函數(shù)
假設(shè)有一張記錄列表,記錄了John到達(dá)工作地點(diǎn)的時(shí)間。如果想知道他這周是否遲到過,那么用any()函數(shù)就十分方便。如果布爾型列表中有一個(gè)元素為True,則any()函數(shù)返回True。
- >>> arrival_hours ={'Mon': 8.5, 'Tue': 8.75, 'Wed': 9, 'Thu': 8.5, 'Fri': 8.5}
- >>> arrival_checks = [x>8.75 for x in arrival_hours.values()]
- >>> any(arrival_checks)
- True
28. 跟蹤列表中元素的頻率
如果還想知道非冠軍玩家在比賽中表現(xiàn)如何,根據(jù)上面的例子,就可以知道第二名和第三名玩家的情況。要想做到這一點(diǎn),需要找出每位玩家的獎(jiǎng)勵(lì)??梢杂米值渖墒胶蛶в衛(wèi)ambda函數(shù)功能的sorted()函數(shù)。
- >>> winnings = ['John','Billy', 'Billy', 'Sam', 'Billy', 'John']
- >>> tracked = {item: winnings.count(item) for item in set(winnings)}
- >>> sorted(tracked.items(), key=lambda x: x[1], reverse=True)
- [('Billy', 3), ('John', 2), ('Sam', 1)]
29. 集體判斷函數(shù)All()
還是用上面的例子,如果還想知道他一周內(nèi)到達(dá)工作地點(diǎn)的時(shí)間是否都在9:30之前,就可以使用all()函數(shù)。只有布爾型列表中所有的元素均為True時(shí),all()函數(shù)才返回True。
- >>> arrival_checks_all =[x<9.5 for x in arrival_hours.values()]
- >>> all(arrival_checks)
- True
30. 用With關(guān)鍵字讀取文件
處理文件時(shí),需要打開文件,處理文件數(shù)據(jù),然后關(guān)閉文件。如果在使用后沒有關(guān)閉文件,過了一段時(shí)間后,就沒法兒讀取該文件。這種情況下,with 關(guān)鍵字非常有用。如下所示,使用后文件將自動(dòng)關(guān)閉。
- >>> withopen('a_file.txt') as file:
- ... pass
- ...
- >>> file.closed
- True
本文并不打算把Python編程中的習(xí)慣用法詳盡地羅列出來,只是想給讀者提供一些常見的習(xí)慣用法,日常編碼中都用得到。