Tip
對你的代碼運行pylint
定義:
pylint是一個在Python源代碼中查找bug的工具. 對于C和C++這樣的不那么動態(tài)的(譯者注: 原文是less dynamic)語言, 這些bug通常由編譯器來捕獲. 由于Python的動態(tài)特性, 有些警告可能不對. 不過偽告警應(yīng)該很少.
優(yōu)點:
可以捕獲容易忽視的錯誤, 例如輸入錯誤, 使用未賦值的變量等.
缺點:
pylint不完美. 要利用其優(yōu)勢, 我們有時侯需要: a) 圍繞著它來寫代碼 b) 抑制其告警 c) 改進它, 或者d) 忽略它.
結(jié)論:
確保對你的代碼運行pylint.抑制不準(zhǔn)確的警告,以便能夠?qū)⑵渌姹┞冻鰜怼?/p>
你可以通過設(shè)置一個行注釋來抑制告警. 例如:
dict = 'something awful' # Bad Idea... pylint: disable=redefined-builtin
pylint警告是以一個數(shù)字編號(如?C0112
?)和一個符號名(如?empty-docstring
?)來標(biāo)識的. 在編寫新代碼或更新已有代碼時對告警進行醫(yī)治, 推薦使用符號名來標(biāo)識.
如果警告的符號名不夠見名知意,那么請對其增加一個詳細(xì)解釋。
采用這種抑制方式的好處是我們可以輕松查找抑制并回顧它們.
你可以使用命令?pylint?--list-msgs
?來獲取pylint告警列表. 你可以使用命令?pylint?--help-msg=C6409
?, 以獲取關(guān)于特定消息的更多信息.
相比較于之前使用的?pylint:?disable-msg
?, 本文推薦使用?pylint:?disable
?.
要抑制”參數(shù)未使用”告警, 你可以用””作為參數(shù)標(biāo)識符, 或者在參數(shù)名前加”unused”. 遇到不能改變參數(shù)名的情況, 你可以通過在函數(shù)開頭”提到”它們來消除告警. 例如:
def foo(a, unused_b, unused_c, d=None, e=None):
_ = d, e
return a
Tip
僅對包和模塊使用導(dǎo)入
定義:
模塊間共享代碼的重用機制.
優(yōu)點:
命名空間管理約定十分簡單. 每個標(biāo)識符的源都用一種一致的方式指示. x.Obj表示Obj對象定義在模塊x中.
缺點:
模塊名仍可能沖突. 有些模塊名太長, 不太方便.
結(jié)論:
使用?import?x
?來導(dǎo)入包和模塊.
使用?from?x?import?y
?, 其中x是包前綴, y是不帶前綴的模塊名.
使用?from?x?import?y?as?z
, 如果兩個要導(dǎo)入的模塊都叫做z或者y太長了.
例如, 模塊?sound.effects.echo
?可以用如下方式導(dǎo)入:
from sound.effects import echo
...
echo.EchoFilter(input, output, delay=0.7, atten=4)
導(dǎo)入時不要使用相對名稱. 即使模塊在同一個包中, 也要使用完整包名. 這能幫助你避免無意間導(dǎo)入一個包兩次.
Tip
使用模塊的全路徑名來導(dǎo)入每個模塊
優(yōu)點:
避免模塊名沖突. 查找包更容易.
缺點:
部署代碼變難, 因為你必須復(fù)制包層次.
結(jié)論:
所有的新代碼都應(yīng)該用完整包名來導(dǎo)入每個模塊.
應(yīng)該像下面這樣導(dǎo)入:
# Reference in code with complete name.
import sound.effects.echo
# Reference in code with just module name (preferred).
from sound.effects import echo
Tip
允許使用異常, 但必須小心
定義:
異常是一種跳出代碼塊的正??刂屏鱽硖幚礤e誤或者其它異常條件的方式.
優(yōu)點:
正常操作代碼的控制流不會和錯誤處理代碼混在一起. 當(dāng)某種條件發(fā)生時, 它也允許控制流跳過多個框架. 例如, 一步跳出N個嵌套的函數(shù), 而不必繼續(xù)執(zhí)行錯誤的代碼.
缺點:
可能會導(dǎo)致讓人困惑的控制流. 調(diào)用庫時容易錯過錯誤情況.
結(jié)論:
異常必須遵守特定條件:
像這樣觸發(fā)異常:?raise?MyException("Error?message")
?或者?raise?MyException
?. 不要使用兩個參數(shù)的形式(?raise?MyException,?"Error?message"
?)或者過時的字符串異常(raise?"Error?message"
?).
class Error(Exception):
pass
永遠不要使用?except:
?語句來捕獲所有異常, 也不要捕獲?Exception
?或者?StandardError
?, 除非你打算重新觸發(fā)該異常, 或者你已經(jīng)在當(dāng)前線程的最外層(記得還是要打印一條錯誤消息). 在異常這方面, Python非常寬容,?except:
?真的會捕獲包括Python語法錯誤在內(nèi)的任何錯誤. 使用?except:
?很容易隱藏真正的bug.
盡量減少try/except塊中的代碼量. try塊的體積越大, 期望之外的異常就越容易被觸發(fā). 這種情況下, try/except塊將隱藏真正的錯誤.
使用finally子句來執(zhí)行那些無論try塊中有沒有異常都應(yīng)該被執(zhí)行的代碼. 這對于清理資源常常很有用, 例如關(guān)閉文件.
as
?而不要用逗號. 例如try:
raise Error
except Error as error:
pass
Tip
避免全局變量
定義:
定義在模塊級的變量.
優(yōu)點:
偶爾有用.
缺點:
導(dǎo)入時可能改變模塊行為, 因為導(dǎo)入模塊時會對模塊級變量賦值.
結(jié)論:
避免使用全局變量, 用類變量來代替. 但也有一些例外:
Tip
鼓勵使用嵌套/本地/內(nèi)部類或函數(shù)
定義:
類可以定義在方法, 函數(shù)或者類中. 函數(shù)可以定義在方法或函數(shù)中. 封閉區(qū)間中定義的變量對嵌套函數(shù)是只讀的.
優(yōu)點:
允許定義僅用于有效范圍的工具類和函數(shù).
缺點:
嵌套類或局部類的實例不能序列化(pickled).
結(jié)論:
推薦使用.
Tip
可以在簡單情況下使用
定義:
列表推導(dǎo)(list comprehensions)與生成器表達式(generator expression)提供了一種簡潔高效的方式來創(chuàng)建列表和迭代器, 而不必借助map(), filter(), 或者lambda.
優(yōu)點:
簡單的列表推導(dǎo)可以比其它的列表創(chuàng)建方法更加清晰簡單. 生成器表達式可以十分高效, 因為它們避免了創(chuàng)建整個列表.
缺點:
復(fù)雜的列表推導(dǎo)或者生成器表達式可能難以閱讀.
結(jié)論:
適用于簡單情況. 每個部分應(yīng)該單獨置于一行: 映射表達式, for語句, 過濾器表達式. 禁止多重for語句或過濾器表達式. 復(fù)雜情況下還是使用循環(huán).
Yes:
result = []
for x in range(10):
for y in range(5):
if x * y > 10:
result.append((x, y))
for x in xrange(5):
for y in xrange(5):
if x != y:
for z in xrange(5):
if y != z:
yield (x, y, z)
return ((x, complicated_transform(x))
for x in long_generator_function(parameter)
if x is not None)
squares = [x * x for x in range(10)]
eat(jelly_bean for jelly_bean in jelly_beans
if jelly_bean.color == 'black')
No:
result = [(x, y) for x in range(10) for y in range(5) if x * y > 10]
return ((x, y, z)
for x in xrange(5)
for y in xrange(5)
if x != y
for z in xrange(5)
if y != z)
Tip
如果類型支持, 就使用默認(rèn)迭代器和操作符. 比如列表, 字典及文件等.
定義:
容器類型, 像字典和列表, 定義了默認(rèn)的迭代器和關(guān)系測試操作符(in和not in)
優(yōu)點:
默認(rèn)操作符和迭代器簡單高效, 它們直接表達了操作, 沒有額外的方法調(diào)用. 使用默認(rèn)操作符的函數(shù)是通用的. 它可以用于支持該操作的任何類型.
缺點:
你沒法通過閱讀方法名來區(qū)分對象的類型(例如, has_key()意味著字典). 不過這也是優(yōu)點.
結(jié)論:
如果類型支持, 就使用默認(rèn)迭代器和操作符, 例如列表, 字典和文件. 內(nèi)建類型也定義了迭代器方法. 優(yōu)先考慮這些方法, 而不是那些返回列表的方法. 當(dāng)然,這樣遍歷容器時,你將不能修改容器.
Yes: for key in adict: ...
if key not in adict: ...
if obj in alist: ...
for line in afile: ...
for k, v in dict.iteritems(): ...
No: for key in adict.keys(): ...
if not adict.has_key(key): ...
for line in afile.readlines(): ...
Tip
按需使用生成器.
定義:
所謂生成器函數(shù), 就是每當(dāng)它執(zhí)行一次生成(yield)語句, 它就返回一個迭代器, 這個迭代器生成一個值. 生成值后, 生成器函數(shù)的運行狀態(tài)將被掛起, 直到下一次生成.
優(yōu)點:
簡化代碼, 因為每次調(diào)用時, 局部變量和控制流的狀態(tài)都會被保存. 比起一次創(chuàng)建一系列值的函數(shù), 生成器使用的內(nèi)存更少.
缺點:
沒有.
結(jié)論:
鼓勵使用. 注意在生成器函數(shù)的文檔字符串中使用”Yields:”而不是”Returns:”.
(譯者注: 參看?注釋?)
Tip
適用于單行函數(shù)
定義:
與語句相反, lambda在一個表達式中定義匿名函數(shù). 常用于為?map()
?和?filter()
?之類的高階函數(shù)定義回調(diào)函數(shù)或者操作符.
優(yōu)點:
方便.
缺點:
比本地函數(shù)更難閱讀和調(diào)試. 沒有函數(shù)名意味著堆棧跟蹤更難理解. 由于lambda函數(shù)通常只包含一個表達式, 因此其表達能力有限.
結(jié)論:
適用于單行函數(shù). 如果代碼超過60-80個字符, 最好還是定義成常規(guī)(嵌套)函數(shù).
對于常見的操作符,例如乘法操作符,使用?operator
?模塊中的函數(shù)以代替lambda函數(shù). 例如, 推薦使用?operator.mul
?, 而不是?lambda?x,?y:?x?*?y
?.
Tip
適用于單行函數(shù)
定義:
條件表達式是對于if語句的一種更為簡短的句法規(guī)則. 例如:?x?=?1?if?cond?else?2
?.
優(yōu)點:
比if語句更加簡短和方便.
缺點:
比if語句難于閱讀. 如果表達式很長, 難于定位條件.
結(jié)論:
適用于單行函數(shù). 在其他情況下,推薦使用完整的if語句.
Tip
適用于大部分情況.
定義:
你可以在函數(shù)參數(shù)列表的最后指定變量的值, 例如,?def?foo(a,?b?=?0):
?. 如果調(diào)用foo時只帶一個參數(shù), 則b被設(shè)為0. 如果帶兩個參數(shù), 則b的值等于第二個參數(shù).
優(yōu)點:
你經(jīng)常會碰到一些使用大量默認(rèn)值的函數(shù), 但偶爾(比較少見)你想要覆蓋這些默認(rèn)值. 默認(rèn)參數(shù)值提供了一種簡單的方法來完成這件事, 你不需要為這些罕見的例外定義大量函數(shù). 同時, Python也不支持重載方法和函數(shù), 默認(rèn)參數(shù)是一種”仿造”重載行為的簡單方式.
缺點:
默認(rèn)參數(shù)只在模塊加載時求值一次. 如果參數(shù)是列表或字典之類的可變類型, 這可能會導(dǎo)致問題. 如果函數(shù)修改了對象(例如向列表追加項), 默認(rèn)值就被修改了.
結(jié)論:
鼓勵使用, 不過有如下注意事項:
不要在函數(shù)或方法定義中使用可變對象作為默認(rèn)值.
Yes: def foo(a, b=None):
if b is None:
b = []
No: def foo(a, b=[]):
...
No: def foo(a, b=time.time()): # The time the module was loaded???
...
No: def foo(a, b=FLAGS.my_thing): # sys.argv has not yet been parsed...
...
Tip
訪問和設(shè)置數(shù)據(jù)成員時, 你通常會使用簡單, 輕量級的訪問和設(shè)置函數(shù). 建議用屬性(properties)來代替它們.
定義:
一種用于包裝方法調(diào)用的方式. 當(dāng)運算量不大, 它是獲取和設(shè)置屬性(attribute)的標(biāo)準(zhǔn)方式.
優(yōu)點:
通過消除簡單的屬性(attribute)訪問時顯式的get和set方法調(diào)用, 可讀性提高了. 允許懶惰的計算. 用Pythonic的方式來維護類的接口. 就性能而言, 當(dāng)直接訪問變量是合理的, 添加訪問方法就顯得瑣碎而無意義. 使用屬性(properties)可以繞過這個問題. 將來也可以在不破壞接口的情況下將訪問方法加上.
缺點:
屬性(properties)是在get和set方法聲明后指定, 這需要使用者在接下來的代碼中注意: set和get是用于屬性(properties)的(除了用?@property
?裝飾器創(chuàng)建的只讀屬性). 必須繼承自object類. 可能隱藏比如操作符重載之類的副作用. 繼承時可能會讓人困惑.
結(jié)論:
你通常習(xí)慣于使用訪問或設(shè)置方法來訪問或設(shè)置數(shù)據(jù), 它們簡單而輕量. 不過我們建議你在新的代碼中使用屬性. 只讀屬性應(yīng)該用?@property
?裝飾器?來創(chuàng)建.
如果子類沒有覆蓋屬性, 那么屬性的繼承可能看上去不明顯. 因此使用者必須確保訪問方法間接被調(diào)用, 以保證子類中的重載方法被屬性調(diào)用(使用模板方法設(shè)計模式).
Yes: import math
class Square(object):
"""A square with two properties: a writable area and a read-only perimeter.
To use:
>>> sq = Square(3)
>>> sq.area
9
>>> sq.perimeter
12
>>> sq.area = 16
>>> sq.side
4
>>> sq.perimeter
16
"""
def __init__(self, side):
self.side = side
def __get_area(self):
"""Calculates the 'area' property."""
return self.side ** 2
def ___get_area(self):
"""Indirect accessor for 'area' property."""
return self.__get_area()
def __set_area(self, area):
"""Sets the 'area' property."""
self.side = math.sqrt(area)
def ___set_area(self, area):
"""Indirect setter for 'area' property."""
self._SetArea(area)
area = property(___get_area, ___set_area,
doc="""Gets or sets the area of the square.""")
@property
def perimeter(self):
return self.side * 4
(譯者注: 老實說, 我覺得這段示例代碼很不恰當(dāng), 有必要這么蛋疼嗎?)
Tip
盡可能使用隱式false
定義:
Python在布爾上下文中會將某些值求值為false. 按簡單的直覺來講, 就是所有的”空”值都被認(rèn)為是false. 因此0, None, [], {}, “” 都被認(rèn)為是false.
優(yōu)點:
使用Python布爾值的條件語句更易讀也更不易犯錯. 大部分情況下, 也更快.
缺點:
對C/C++開發(fā)人員來說, 可能看起來有點怪.
結(jié)論:
盡可能使用隱式的false, 例如: 使用?if?foo:
?而不是?if?foo?!=?[]:
?. 不過還是有一些注意事項需要你銘記在心:
永遠不要用==或者!=來比較單件, 比如None. 使用is或者is not.
注意: 當(dāng)你寫下?if?x:
?時, 你其實表示的是?if?x?is?not?None
?. 例如: 當(dāng)你要測試一個默認(rèn)值是None的變量或參數(shù)是否被設(shè)為其它值. 這個值在布爾語義下可能是false!
永遠不要用==將一個布爾量與false相比較. 使用?if?not?x:
?代替. 如果你需要區(qū)分false和None, 你應(yīng)該用像?if?not?x?and?x?is?not?None:
?這樣的語句.
對于序列(字符串, 列表, 元組), 要注意空序列是false. 因此?if?not?seq:
?或者?if?seq:
?比?if?len(seq):
?或?if?not?len(seq):
?要更好.
Yes: if not users:
print 'no users'
if foo == 0:
self.handle_zero()
if i % 10 == 0:
self.handle_multiple_of_ten()
No: if len(users) == 0:
print 'no users'
if foo is not None and not foo:
self.handle_zero()
if not i % 10:
self.handle_multiple_of_ten()
Tip
盡可能使用字符串方法取代字符串模塊. 使用函數(shù)調(diào)用語法取代apply(). 使用列表推導(dǎo), for循環(huán)取代filter(), map()以及reduce().
定義:
當(dāng)前版本的Python提供了大家通常更喜歡的替代品.
結(jié)論:
我們不使用不支持這些特性的Python版本, 所以沒理由不用新的方式.
Yes: words = foo.split(':')
[x[1] for x in my_list if x[2] == 5]
map(math.sqrt, data) # Ok. No inlined lambda expression.
fn(*args, **kwargs)
No: words = string.split(foo, ':')
map(lambda x: x[1], filter(lambda x: x[2] == 5, my_list))
apply(fn, args, kwargs)
Tip
推薦使用
定義:
嵌套的Python函數(shù)可以引用外層函數(shù)中定義的變量, 但是不能夠?qū)λ鼈冑x值. 變量綁定的解析是使用詞法作用域, 也就是基于靜態(tài)的程序文本. 對一個塊中的某個名稱的任何賦值都會導(dǎo)致Python將對該名稱的全部引用當(dāng)做局部變量, 甚至是賦值前的處理. 如果碰到global聲明, 該名稱就會被視作全局變量.
一個使用這個特性的例子:
def get_adder(summand1):
"""Returns a function that adds numbers to a given number."""
def adder(summand2):
return summand1 + summand2
return adder
(譯者注: 這個例子有點詭異, 你應(yīng)該這樣使用這個函數(shù):?sum?=?get_adder(summand1)(summand2)
?)
優(yōu)點:
通??梢詭砀忧逦? 優(yōu)雅的代碼. 尤其會讓有經(jīng)驗的Lisp和Scheme(還有Haskell, ML等)程序員感到欣慰.
缺點:
可能導(dǎo)致讓人迷惑的bug. 例如下面這個依據(jù)?PEP-0227?的例子:
i = 4
def foo(x):
def bar():
print i,
# ...
# A bunch of code here
# ...
for i in x: # Ah, i *is* local to Foo, so this is what Bar sees
print i,
bar()
因此?foo([1,?2,?3])
?會打印?1?2?3?3
?, 不是?1?2?3?4
?.
(譯者注: x是一個列表, for循環(huán)其實是將x中的值依次賦給i.這樣對i的賦值就隱式的發(fā)生了, 整個foo函數(shù)體中的i都會被當(dāng)做局部變量, 包括bar()中的那個. 這一點與C++之類的靜態(tài)語言還是有很大差別的.)
結(jié)論:
鼓勵使用.
Tip
如果好處很顯然, 就明智而謹(jǐn)慎的使用裝飾器
定義:
用于函數(shù)及方法的裝飾器?(也就是@標(biāo)記). 最常見的裝飾器是@classmethod 和@staticmethod, 用于將常規(guī)函數(shù)轉(zhuǎn)換成類方法或靜態(tài)方法. 不過, 裝飾器語法也允許用戶自定義裝飾器. 特別地, 對于某個函數(shù)?my_decorator
?, 下面的兩段代碼是等效的:
class C(object):
@my_decorator
def method(self):
# method body ...
class C(object):
def method(self):
# method body ...
method = my_decorator(method)
優(yōu)點:
優(yōu)雅的在函數(shù)上指定一些轉(zhuǎn)換. 該轉(zhuǎn)換可能減少一些重復(fù)代碼, 保持已有函數(shù)不變(enforce invariants), 等.
缺點:
裝飾器可以在函數(shù)的參數(shù)或返回值上執(zhí)行任何操作, 這可能導(dǎo)致讓人驚異的隱藏行為. 而且, 裝飾器在導(dǎo)入時執(zhí)行. 從裝飾器代碼的失敗中恢復(fù)更加不可能.
結(jié)論:
如果好處很顯然, 就明智而謹(jǐn)慎的使用裝飾器. 裝飾器應(yīng)該遵守和函數(shù)一樣的導(dǎo)入和命名規(guī)則. 裝飾器的python文檔應(yīng)該清晰的說明該函數(shù)是一個裝飾器. 請為裝飾器編寫單元測試.
避免裝飾器自身對外界的依賴(即不要依賴于文件, socket, 數(shù)據(jù)庫連接等), 因為裝飾器運行時這些資源可能不可用(由?pydoc
?或其它工具導(dǎo)入). 應(yīng)該保證一個用有效參數(shù)調(diào)用的裝飾器在所有情況下都是成功的.
裝飾器是一種特殊形式的”頂級代碼”. 參考后面關(guān)于?Main?的話題.
Tip
不要依賴內(nèi)建類型的原子性.
雖然Python的內(nèi)建類型例如字典看上去擁有原子操作, 但是在某些情形下它們?nèi)匀徊皇窃拥?即: 如果hash或eq被實現(xiàn)為Python方法)且它們的原子性是靠不住的. 你也不能指望原子變量賦值(因為這個反過來依賴字典).
優(yōu)先使用Queue模塊的?Queue
?數(shù)據(jù)類型作為線程間的數(shù)據(jù)通信方式. 另外, 使用threading模塊及其鎖原語(locking primitives). 了解條件變量的合適使用方式, 這樣你就可以使用?threading.Condition
?來取代低級別的鎖了.
Tip
避免使用這些特性
定義:
Python是一種異常靈活的語言, 它為你提供了很多花哨的特性, 諸如元類(metaclasses), 字節(jié)碼訪問, 任意編譯(on-the-fly compilation), 動態(tài)繼承, 對象父類重定義(object reparenting), 導(dǎo)入黑客(import hacks), 反射, 系統(tǒng)內(nèi)修改(modification of system internals), 等等.
優(yōu)點:
強大的語言特性, 能讓你的代碼更緊湊.
缺點:
使用這些很”酷”的特性十分誘人, 但不是絕對必要. 使用奇技淫巧的代碼將更加難以閱讀和調(diào)試. 開始可能還好(對原作者而言), 但當(dāng)你回顧代碼, 它們可能會比那些稍長一點但是很直接的代碼更加難以理解.
結(jié)論:
在你的代碼中避免這些特性.
更多建議: