標籤:blog data 插入 操作 http print ble 導致 arch
原文連結:http://blog.guoyb.com/2016/12/03/bad-py-style/
最近在看一些陳年老系統,其中有一些不好的代碼習慣遺留下來的坑;加上最近自己也寫了一段爛代碼導致伺服器負載飆升,所以就趁此機會總結下我看到過/寫過的自認為不好的 Python 代碼習慣,時刻提醒自己遠離這些“最差實踐”,避免挖坑。
下面所舉的例子中,有一部分會造成效能問題,有一部分會導致隱藏 bug,或日後維護、重構困難,還有一部分純粹是我認為不夠 pythonic。所以大家自行甄別,取精去糟吧。
函數預設參數使用可變對象
這個例子我想大家應該在各種技術文章中見過許多遍了,也足以證明這是一個大坑。
先看錯誤示範吧:
def use_mutable_default_param(idx=0, ids=[]): ids.append(idx) print(idx) print(ids)use_mutable_default_param(idx=1)use_mutable_default_param(idx=2)輸出:1[1]2[1, 2]
理解這其中的原因,最重要的是有兩點:
函數本身也是一個對象,預設參數綁定於這個函數對象上
append 這類方法會直接修改對象,所以下次調用此函數時,其綁定的預設參數已經不再是空list了
正確的做法如下:
def donot_use_mutable_default_param(idx=0, ids=None): if ids is None: ids = [] ids.append(idx) print(idx) print(ids)
try…except不具體指明異常類型
雖然在 Python 中使用 try…except 不會帶來嚴重的效能問題,但是不加區分,直接捕獲所有類型異常的做法,往往會掩蓋掉其他的 bug,造成難以追查的 bug。
一般的,我覺得應該盡量少的使用 try…except,這樣可以在開發期儘早的發現問題。即使要使用 try…except,也應該儘可能的指定出要捕獲的具體異常,並在 except 語句中將異常資訊記入 log,或者處理完之後,再直接raise出來。
關於dict的冗餘代碼
我經常能夠看到這樣的代碼:
d = {}datas = [1, 2, 3, 4, 2, 3, 4, 1, 5]for k in datas: if k not in d: d[k] = 0 d[k] += 1
其實,完全可以使用 collections.defaultdict 這一資料結構更簡單優雅的實現這樣的功能:
default_d = defaultdict(lambda: 0)datas = [1, 2, 3, 4, 2, 3, 4, 1, 5]for k in datas: default_d[k] += 1
同樣的,這樣的代碼:
# d is a dictif 'list' not in d:d['list'] = []d['list'].append(x)
完全可以用這樣一行代碼替代:
# d is a dictd.setdefault('list', []).append(x)
同樣的,下面這兩種寫法一看就是帶有濃濃的C味兒:
# d is a dictfor k in d:v = d[k]# do something# l is a listfor i in len(l):v = l[i]# do something
應該用更 pythonic 的寫法:
# d is a dictfor k, v in d.iteritems():# do somethingpass# l is a listfor i, v in enumerate(l):# do somethingpass
另外,enumerate 其實還有個第二參數,表示序號從幾開始。如果想要序號從1開始數起,可以使用 enumerate(l, 1)。
使用flag變數而不使用for…else語句
同樣,這樣的代碼也很常見:
search_list = ['Jone', 'Aric', 'Luise', 'Frank', 'Wey']found = Falsefor s in search_list: if s.startswith('C'): found = True # do something when found print('Found') breakif not found: # do something when not found print('Not found')
其實,用 for…else 更優雅:
search_list = ['Jone', 'Aric', 'Luise', 'Frank', 'Wey']for s in search_list: if s.startswith('C'): # do something when found print('Found') breakelse: # do something when not found print('Not found')
過度使用 tuple unpacking
在 Python 中,允許對 tuple 類型進行 unpack 操作,如下所示:
# human = ('James', 180, 32)name,height,age = human
這個特性用起來很爽,比寫 name=human[0] 之類的不知道高到哪裡去了。所以,這一特性往往被濫用,一個 human 在程式的各處通過上面的方式 unpack。
然而如果後來需要在 human 中插入一個表示性別的資料 sex,那麼對於所有的這種 unpack 都需要進行修改,即使在有些邏輯中並不會使用到性別。
# human = ('James', 180, 32)name,height,age, _ = human# or# name, height, age, sex = human
有如下幾種方式解決這一問題:
老老實實寫 name=human[0] 這種代碼,在需要使用性別資訊處加上 sex=human[3]
使用 dict 來表示 human
使用 namedtuple
# human = namedtuple('human', ['name', 'height', 'age', 'sex'])h = human('James', 180, 32, 0)# then you can use h.name, h.sex and so on everywhere.
到處都是 import *
import * 是一種懶惰的行為,它不僅會汙染當前的命名空間,並且還會使得 pyflakes 等代碼檢查工具失效。在後續查看代碼或者 debug 的過程中,往往也很難從一堆 import * 中找到一個第三方函數的來源。
可以說這種習慣是百害而無一利的。
檔案操作
檔案操作不要使用裸奔的f = open(‘filename’)了,使用with open(‘filename’) as f來讓context manager幫你處理異常情況下的關閉檔案等亂七八糟的事情多好。
野蠻使用 class.name 判斷類型
我曾經遇見過一個 bug:為了實現某特定功能,我新寫了一個 class B(A),在 B 中重寫了 A 的若干函數。整個實現很簡單,但是就是有一部分 A 的功能無法生效。最後追查到的原因,就是在一些邏輯代碼中,硬性的判斷了entity.class.name == ‘A’。
除非你就是想限定死繼承層級中的當前類型(也就是,屏蔽未來可能會出現的子類),否則,不要使用 class.name,而改用 isinstance 這個內建函數。畢竟,Python 把這兩個變數的名字都刻意帶上那麼多底線,本來就是不太想讓你用嘛。
迴圈內部有多層函數調用
迴圈內部有多層函數調用,有如下兩方面的隱患:
Python 沒有 inline 函數,所以函數調用本來就會導致一定的開銷,尤其是本身邏輯簡單的時候,這個開銷所佔的比例就會挺可觀的。
更嚴重的是,在之後維護這份代碼時,會容易讓人忽略掉函數是在迴圈中被調用的,所以容易在函數內部添加了一些開銷較大卻不必每次迴圈都調用的函數,比如 time.localtime()。如果是直接一個平鋪直敘的迴圈,我想大部分的程式員都應該知道把 time.localtime()寫到迴圈的外面,但是引入多層的函數調用之後,就不一定了哦。
所以我建議,在迴圈內部,如非特別複雜的邏輯,都應該直接寫在迴圈裡,不要進行函數調用。如果一定要封裝一層函數調用,應該在函數的命名或注釋中,提示後續的維護者,這個函數會在迴圈內部使用。
Python 是一門非常容易入門的語言,嚴格的縮排要求和豐富的內建資料類型,使得大部分 Python 代碼都能做到比較好的規範。但是,不嚴格要求自己,也很容易就寫出犯二的代碼。上面列出的只是很小的一部分,唯有多讀、多寫、多想,才能培養敏銳的代碼嗅覺,第一時間發現壞味道啊。歡迎大家補充~
遠離 Python 最差實踐,避免挖坑