Python 最差實踐

一根笨茄子發表於2016-12-04

最近在看一些陳年老系統,其中有一些不好的程式碼習慣遺留下來的坑;加上最近自己也寫了一段爛程式碼導致伺服器負載飆升,所以就趁此機會總結下我看到過/寫過的自認為不好的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]

理解這其中的原因,最重要的是有兩點:

  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 dict
if 'list' not in d:
	d['list'] = []
d['list'].append(x)

完全可以用這樣一行程式碼替代:

# d is a dict
d.setdefault('list', []).append(x)

同樣的,下面這兩種寫法一看就是帶有濃濃的C味兒:

# d is a dict
for k in d:
	v = d[k]
	# do something

# l is a list
for i in len(l):
	v = l[i]
	# do something

應該用更pythonic的寫法:

# d is a dict
for k, v in d.iteritems():
	# do something
	pass

# l is a list
for i, v in enumerate(l):
	# do something
	pass

另外,enumerate其實還有個第二引數,表示序號從幾開始。如果想要序號從1開始數起,可以使用enumerate(l, 1)。

使用flag變數而不使用for…else語句

同樣,這樣的程式碼也很常見:

search_list = ['Jone', 'Aric', 'Luise', 'Frank', 'Wey']
found = False
for s in search_list:
    if s.startswith('C'):
        found = True
        # do something when found
        print('Found')
        break

if 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')
        break
else:
    # 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

有如下幾種方式解決這一問題:

  1. 老老實實寫name=human[0]這種程式碼,在需要使用性別資訊處加上sex=human[3]
  2. 使用dict來表示human
  3. 使用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把這兩個變數的名字都刻意帶上那麼多下劃線,本來就是不太想讓你用嘛。

迴圈內部有多層函式呼叫

迴圈內部有多層函式呼叫,有如下兩方面的隱患:

  1. Python沒有inline函式,所以函式呼叫本來就會導致一定的開銷,尤其是本身邏輯簡單的時候,這個開銷所佔的比例就會挺可觀的。
  2. 更嚴重的是,在之後維護這份程式碼時,會容易讓人忽略掉函式是在迴圈中被呼叫的,所以容易在函式內部新增了一些開銷較大卻不必每次迴圈都呼叫的函式,比如time.localtime()。如果是直接一個平鋪直敘的迴圈,我想大部分的程式設計師都應該知道把time.localtime()寫到迴圈的外面,但是引入多層的函式呼叫之後,就不一定了哦。

所以我建議,在迴圈內部,如非特別複雜的邏輯,都應該直接寫在迴圈裡,不要進行函式呼叫。如果一定要包裝一層函式呼叫,應該在函式的命名或註釋中,提示後續的維護者,這個函式會在迴圈內部使用。

Python是一門非常容易入門的語言,嚴格的縮排要求和豐富的內建資料型別,使得大部分Python程式碼都能做到比較好的規範。但是,不嚴格要求自己,也很容易就寫出犯二的程式碼。上面列出的只是很小的一部分,唯有多讀、多寫、多想,才能培養敏銳的程式碼嗅覺,第一時間發現壞味道啊。

相關文章