python編碼最佳實踐之總結
該文章轉自阿里巴巴技術協會(ATA)
作者:空溟
相信用python的同學不少,本人也一直對python情有獨鍾,毫無疑問python作為一門解釋性動態語言沒有那些編譯型語言高效,但是python簡潔、易讀以及可擴充套件性等特性使得它大受青睞。
工作中很多同事都在用python,但往往很少有人關注它的效能和慣用法,一般都是現學現用,畢竟python不是我們的主要語言,我們一般只是使用它來做一些系統管理的工作。但是我們為什麼不做的更好呢?python zen中有這樣一句:There should be one– and preferably only one –obvious way to do it. Although that way may not be obvious at first unless you`re Dutch. 大意就是python鼓勵使用一種最優的方法去完成一件事,這也是和ruby等的一個差異。所以一種好的python編寫習慣個人認為很重要,本文就重點從效能角度出發對python的一些慣用法做一個簡單總結,希望對大家有用~
提到效能,最容易想到的是降低複雜度,一般可以通過測量程式碼迴路複雜度(cyclomatic complexitly)和Landau符號(大O)來分析, 比如dict查詢是O(1),而列表的查詢卻是O(n),顯然資料的儲存方式選擇會直接影響演算法的複雜度。
一、資料結構的選擇:
1. 在列表中查詢:
對於已經排序的列表考慮用bisect模組來實現查詢元素,該模組將使用二分查詢實現
def find(seq, el) : pos = bisect(seq, el) if pos == 0 or ( pos == len(seq) and seq[-1] != el ) : return -1 return pos - 1
而快速插入一個元素可以用:
bisect.insort(list, element)
這樣就插入元素並且不需要再次呼叫 sort() 來保序,要知道對於長list代價很高.
2. set代替列表:
比如要對一個list進行去重,最容易想到的實現:
seq = [`a`, `a`, `b`] res = [] for i in seq: if i not in res: res.append(i)
顯然上面的實現的複雜度是O(n2),若改成:
seq = [`a`, `a`, `b`] res = set(seq)
複雜度馬上降為O(n),當然這裡假定set可以滿足後續使用。
另外,set的union,intersection,difference等操作要比列表的迭代快的多,因此如果涉及到求列表交集,並集或者差集等問題可以轉換為set來進行,平時使用的時候多注意下,特別當列表比較大的時候,效能的影響就更大。
3. 使用python的collections模組替代內建容器型別:
collections有三種型別:
- deque:增強功能的類似list型別
- defaultdict:類似dict型別
- namedtuple:類似tuple型別
列表是基於陣列實現的,而deque是基於雙連結串列的,所以後者在中間or前面插入元素,或者刪除元素都會快很多。
defaultdict為新的鍵值新增了一個預設的工廠,可以避免編寫一個額外的測試來初始化對映條目,比dict.setdefault更高效,引用python文件的一個例子:
#使用profile stats工具進行效能分析
>>> from pbp.scripts.profiler import profile, stats >>> s = [(`yellow`, 1), (`blue`, 2), (`yellow`, 3), ... (`blue`, 4), (`red`, 1)] >>> @profile(`defaultdict`) ... def faster(): ... d = defaultdict(list) ... for k, v in s: ... d[k].append(v) ... >>> @profile(`dict`) ... def slower(): ... d = {} ... for k, v in s: ... d.setdefault(k, []).append(v) ... >>> slower(); faster() Optimization: Solutions [ 306 ] >>> stats[`dict`] {`stones`: 16.587882671716077, `memory`: 396, `time`: 0.35166311264038086} >>> stats[`defaultdict`] {`stones`: 6.5733464259021686, `memory`: 552, `time`: 0.13935494422912598}
可見效能提升了快3倍。defaultdict用一個list工廠作為引數,同樣可用於內建型別,比如long等。
除了實現的演算法、架構之外,python提倡簡單、優雅。所以正確的語法實踐又很有必要,這樣才會寫出優雅易於閱讀的程式碼。
二、語法最佳實踐:
- 字串操作:優於python字串物件是不可改變的,因此對任何字串的操作如拼接,修改等都將產生一個新的字串物件,而不是基於原字串,因此這種持續的 copy會在一定程度上影響Python的效能:
(1)用join代替 `+` 操作符,後者有copy開銷;
(2)同時當對字串可以使用正規表示式或者內建函式來處理的時候,選擇內建函式。如str.isalpha(),str.isdigit(),str.startswith((‘x’, ‘yz’)),str.endswith((‘x’, ‘yz’))
(3)字元格式化操作優於直接串聯讀取:
str = "%s%s%s%s" % (a, b, c, d) # efficient str = "" + a + b + c + d + "" # slow
2. 善用list comprehension(列表解析) & generator(生成器) & decorators(裝飾器),熟悉itertools等模組:
(1) 列表解析,我覺得是python2中最讓我印象深刻的特性,舉例1:
>>> # the following is not so Pythonic >>> numbers = range(10) >>> i = 0 >>> evens = [] >>> while i < len(numbers): >>> if i %2 == 0: evens.append(i) >>> i += 1 >>> [0, 2, 4, 6, 8] >>> # the good way to iterate a range, elegant and efficient >>> evens = [ i for i in range(10) if i%2 == 0] >>> [0, 2, 4, 6, 8]
舉例2:
def _treament(pos, element): return `%d: %s` % (pos, element)
f = open(`test.txt`, `r`) if __name__ == `__main__`: #list comps 1 print sum(len(word) for line in f for word in line.split()) #list comps 2 print [(x + 1, y + 1) for x in range(3) for y in range(4)] #func print filter(lambda x: x % 2 == 0, range(10)) #list comps3 print [i for i in range(10) if i % 2 == 0] #list comps4 pythonic print [_treament(i, el) for i, el in enumerate(range(10))] output: 24 [(1, 1), (1, 2), (1, 3), (1, 4), (2, 1), (2, 2), (2, 3), (2, 4), (3, 1), (3, 2), (3, 3), (3, 4)] [0, 2, 4, 6, 8] [0, 2, 4, 6, 8] [`0: 0`, `1: 1`, `2: 2`, `3: 3`, `4: 4`, `5: 5`, `6: 6`, `7: 7`, `8: 8`, `9: 9`]
沒錯,就是這麼優雅簡單。
(2) 生成器表示式在python2.2引入,它使用`lazy evaluation`思想,因此在使用記憶體上更有效。引用python核心程式設計中計算檔案中最長的行的例子:
f = open(`/etc/motd, `r`) longest = max(len(x.strip()) for x in f) f.close() return longest
這種實現簡潔而且不需要把檔案檔案所有行讀入記憶體。
(3) python在2.4引入裝飾器,又是一個讓人興奮的特性,簡單來說它使得函式和方法封裝(接收一個函式並返回增強版本的函式)更容易閱讀、理解。`@`符號是裝飾器語法,你可以裝飾一個函式,記住呼叫結果供後續使用,這種技術被稱為memoization的,下面是用裝飾器完成一個cache功能:
import time import hashlib import pickle from itertools import chain cache = {} def is_obsolete(entry, duration): return time.time() - entry[`time`] > duration def compute_key(function, args, kw): #序列化/反序列化一個物件,這裡是用pickle模組對函式和引數物件進行序列化為一個hash值 key = pickle.dumps((function.func_name, args, kw)) #hashlib是一個提供MD5和sh1的一個庫,該結果儲存在一個全域性字典中 return hashlib.sha1(key).hexdigest() def memoize(duration=10): def _memoize(function): def __memoize(*args, **kw): key = compute_key(function, args, kw) # do we have it already if (key in cache and not is_obsolete(cache[key], duration)): print `we got a winner` return cache[key][`value`] # computing result = function(*args, **kw) # storing the result cache[key] = {`value`: result,- `time`: time.time()} return result return __memoize return _memoize @memoize() def very_very_complex_stuff(a, b, c): return a + b + c print very_very_complex_stuff(2, 2, 2) print very_very_complex_stuff(2, 2, 2) @memoize(1) def very_very_complex_stuff(a, b): return a + b print very_very_complex_stuff(2, 2) time.sleep(2) print very_very_complex_stuff(2, 2)
執行結果:
6
we got a winner
6
4
4
裝飾器在很多場景用到,比如引數檢查、鎖同步、單元測試框架等,有興趣的人可以自己進一步學習。
3. 善用python強大的自省能力(屬性和描述符):自從使用了python,真的是驚訝原來自省可以做的這麼強大簡單,關於這個話題,限於內容比較多,這裡就不贅述,後續有時間單獨做一個總結,學習python必須對其自省好好理解。
三、 編碼小技巧:
- 在python3之前版本使用xrange代替range,因為range()直接返回完整的元素列表而xrange()在序列中每次呼叫只產生一個整數元素,開銷小。(在python3中xrange不再存在,裡面range提供一個可以 遍歷任意長度的範圍的iterator)
- if done is not None比語句if done != None更快;
- 儘量使用”in”操作符,簡潔而快速: for i in seq: print i
- `x < y < z`代替`x < y and y < z`;
- while 1要比while True更快, 因為前者是單步運算,後者還需要計算;
- 儘量使用build-in的函式,因為這些函式往往很高效,比如add(a,b)要優於a+b;
- 在耗時較多的迴圈中,可以把函式的呼叫改為內聯的方式,內迴圈應該保持簡潔。
-
使用多重賦值來swap元素:
x, y = y, x # elegant and efficient
而不是:
temp = x x = y y = temp
9. 三元操作符(python2.5後):V1 if X else V2,避免使用(X and V1) or V2,因為後者當V1=””時,就會有問題。
10. python之switch case實現:因為switch case語法完全可用if else代替,所以python就沒 有switch case語法,但是我們可以用dictionary或lamda實現:
switch case結構:
switch (var) { case v1: func1(); case v2: func2(); ... case vN: funcN(); default: default_func(); }
dictionary實現:
values = { v1: func1, v2: func2, ... vN: funcN, } values.get(var, default_func)()
lambda實現:
{ `1`: lambda: func1, `2`: lambda: func2, `3`: lambda: func3 }[value]()
用try…catch來實現帶Default的情況,個人推薦使用dict的實現方法。
這裡只總結了一部分python的實踐方法,希望這些建議可以幫助到每一位使用python的同學,優化效能不是重點,高效解決問題,讓自己寫的程式碼更加易於維護,更加pythonic!
相關文章
- 智慧合約最佳實踐 之 Solidity 編碼規範Solid
- 編碼最佳實踐——介面分離原則
- 編寫優雅程式碼的最佳實踐
- dart系列之:dart程式碼最佳實踐Dart
- 編譯提速最佳實踐編譯
- 什麼是框架?(最佳實踐、專案總結)框架
- 編碼最佳實踐——開放封閉原則
- 編碼最佳實踐——單一職責原則
- Nestjs最佳實踐教程:1編碼環境搭建JS
- Vert.x 程式碼結構最佳實踐
- dart系列之:手寫Library,Library編寫最佳實踐Dart
- springcloud之hystrix原理和實踐總結SpringGCCloud
- 關於 JS 模組化的最佳實踐總結JS
- 編寫高效能 Java 程式碼的最佳實踐Java
- 【翻譯】編寫程式碼註釋的最佳實踐
- Kotlin 初嘗之專案實踐總結Kotlin
- 【GoLang】golang 最佳實踐彙總Golang
- 個人總結——全面的『Python編碼規範』Python
- 博文推薦|Pulsar 客戶端編碼最佳實踐客戶端
- 編寫 Android Library 的最佳實踐Android
- Linux實踐總結Linux
- Shell指令碼最佳實踐指令碼
- Java最佳實踐小結 - jonathangilesJava
- Vue開發總結 及 一些最佳實踐 (已更新)Vue
- Mybatis plus通用欄位自動填充的最佳實踐總結MyBatis
- 【總結】五款頂級Python程式碼編輯器!Python
- 通義靈碼實踐教程——編碼使用實踐
- RESTful API實踐總結RESTAPI
- 2019年終總結之SAP專案實踐篇
- 2018年終總結之SAP專案實踐篇
- Laravel 編碼實踐分享Laravel
- 老外總結的14條Go介面最佳實踐,有些不一樣Go
- 前端乾貨之JS最佳實踐前端JS
- PHP最佳實踐之資料庫PHP資料庫
- dart系列之:集合使用最佳實踐Dart
- 編寫git commit資訊的最佳實踐GitMIT
- 編寫架構文件的最佳實踐 - Singh架構
- 《程式設計實踐》第一章編碼風格的不完全總結程式設計
- Python 連結串列實踐Python