Python 是谷歌主要使用的動態(tài)語言,本風格指導列舉了使用 Python 編程時應該做和不該做的事項(dos & don'ts)
1 背景
為了幫助你正確地組織代碼,我們編寫了一個 Vim 的設(shè)置文件.對于 Emacs,默認設(shè)置即可.
許多團隊使用 yapf 自動格式工具來避免格式爭議
2 Python語言規(guī)則
2.1 Lint
對代碼使用pylint
2.1.1Definition(以下都譯為定義)
pylint 是一個用于在 Python 代碼中發(fā)現(xiàn)bug和代碼風格問題的工具,,pylint 查找那些常在非動態(tài)語言(例如 C 或 C++)編譯器中捕獲的問題.由于 Python 是動態(tài)語言,一些警告可能不正確,不過應該非常少有錯誤警告.
2.1.2 Pros
能夠發(fā)現(xiàn)一些易被遺漏的錯誤,類似拼寫錯誤,調(diào)用早于聲明等等.
2.1.3 Cons
pylint 并不完美,為了更好的利用工具,我們有時候需要
a. Write around it(適配上下文風格)
b. 壓制一些警告
c. 優(yōu)化工具
2.1.4 Decision(以下都譯為建議)
確保對代碼應用 pylint
如果一些警告是不合適的,就抑制這些警告,這是為了讓其他警告不會被隱藏.為了壓制警告,可以設(shè)置行級別的注釋:
dict = 'something awful' # Bad Idea... pylint: disable=redefined-builtin
pylint 警告包含標識名(empty-docstring),谷歌專有的警告以 g-開頭.
如果抑制警告的原因在標識名稱中表述不夠清晰,請額外添加注解.
用這種方式來抑制警告的優(yōu)點是我們能夠簡單地找到抑制的警告并且重新訪問這些警告.
可以通過下述方式來獲得 pylint 警告列表:
pylint --list-msgs
用下述方式來獲取某個特定消息的更多具體信息:
pylint --help-msg=C6409
優(yōu)先使用 pylint: disable 而非舊方法(pylint: disable-msg)如果要抑制由于參數(shù)未使用的警告,可以在函數(shù)開頭 del,并注釋為什么要刪除這些未使用參數(shù),僅僅一句"unused"是不夠的:
def viking_cafe_order(spam, beans, eggs=None):
del beans, eggs # Unused by vikings.
return spam + spam + spa
其他可以用來抑制警告的方式包括用 '_' 作為未使用參數(shù)的標識,在參數(shù)名前增加'unused_',或者分配這些參數(shù)到 '_'.這些方式是可以的,但是已經(jīng)不鼓勵繼續(xù)使用.前兩種方式會影響到通過參數(shù)名傳參的調(diào)用方式,而最后一種并不能保證參數(shù)確實未被使用.
2.2 Imports
只在 import 包和模塊的時候使用 import,而不要應用在單獨的類或函數(shù).(這一條對于 typing_module 有特別的意外)
2.2.1 定義
一個模塊到另一個模塊之間共享代碼的復用性機制
2.2.2 Pros
命名空間管理約定簡單,每個標識的源都一致性地被指明了.例如 x.Obj 表示 Obj 是在模塊x中定義的
2.2.3 Cons
模塊名可能會有沖突,一些模塊名可能很長,比較不方便
2.2.4 建議
- import x(當x是包或模塊)
- from x import y (當x是包前綴,y是不帶前綴的模塊名)
- from x import y as z (當有重復模塊名y或y過長不利于引用的時候)
- import y as z (僅在非常通用的簡寫的時候使用例如import numpy as np)
以 sound.effects.echo 為例:
from sound.effects import echo...echo.EchoFilter(input, output, delay=0.7, atten=4)
不要使用相對引用,即便在同一包內(nèi),也使用完整包名 import,這有助于避免無意重復 import 包.
從 typing module 和 six.moves module import 不適用上述規(guī)則
2.3 包
每一模塊都要從完整路徑 import
2.3.1 Pros
能夠避免模塊名沖突以及由于模塊搜索路徑與作者預期不符而造成的錯誤引用.讓查找模塊更簡單.
2.3.2 Cons
讓部署代碼時有些困難,因為包架構(gòu)也需要賦值,不過對于現(xiàn)在的部署機制而言,這其實不是問題.
2.3.3 建議
所有的新代碼都要從完整包名來 import 模塊
import 示例應該像這樣:
Yes:
# Reference absl.flags in code with the complete name (verbose).
# 在代碼中使用完整路徑調(diào)用absl.flags
import absl.flagsfrom doctor.who import jodie
FLAGS = absl.flags.FLAGS
# Reference flags in code with just the module name (common).
# 在代碼中只用包名來調(diào)用flags
from absl import flagsfrom doctor.who import jodie
FLAGS = flags.FLAGS
No:(假設(shè)文件在 doctor/who 中,jodie.py 也在這里)
# Unclear what module the author wanted and what will be imported. The actual
# import behavior depends on external factors controlling sys.path.
# Which possible jodie module did the author intend to import?
# 不清楚作者想要哪個包以及最終import的是哪個包,
# 實際的import操作依賴于受到外部參數(shù)控制的sys.path
# 那么哪一個可能的jodie模塊是作者希望import的呢?
import jodie
不應該假設(shè)主代碼所在路徑被包含在 sys.path 中,即使有些時候可以 work.在上一例代碼中,我們應該認為 import jodie 指的是 import 一個叫做 jodie 的第三方包或者頂級目錄中的 jodie,而非一個當前路徑的 jodie.py
2.4 異常
異常處理是允許使用的,但使用務(wù)必謹慎
2.4.1 定義
異常是一種從正常代碼段控制流中跳出以處理錯誤或者其他異常條件的手段.
2.4.2 Pros
正常代碼的控制流時不會被錯誤處理代碼影響的.異常處理同樣允許在某些情況下,控制流跳過多段代碼,例如在某一步從N個嵌入函數(shù)返回結(jié)果而非強行延續(xù)錯誤代碼.
2.4.3 Cons
可能會讓控制流變的難于理解,也比較容易錯過調(diào)用庫函數(shù)的報錯.
2.4.4 建議
異常必定遵循特定條件:
- 使用 raise MyError('Error message')或者 raise MyError(),不要使用兩段 raise MyError, 'Error message'
- 當內(nèi)置異常類合理的時候,盡量使用內(nèi)置異常.例如:拋出 ValueError 來表示一個像是違反預設(shè)前提(例如傳參了一個負數(shù)給要求正數(shù)的情況)的程序錯誤發(fā)生.
不要使用 assert 來片段公共結(jié)構(gòu)參數(shù)值 .assert 是用來確認內(nèi)部計算正確性也不是用來表示一些預期外的事件發(fā)生的.如果異常是后續(xù)處理要求的,用 raise 語句來處理,例如:
Yes:
def connect_to_next_port(self, minimum):
"""Connects to the next available port.
Args:
minimum: A port value greater or equal to 1024.
Returns:
The new minimum port.
Raises:
ConnectionError: If no available port is found.
"""
if minimum < 1024:
# Note that this raising of ValueError is not mentioned in the doc
# string's "Raises:" section because it is not appropriate to
# guarantee this specific behavioral reaction to API misuse.
# 注意拋出ValueError這件事是不在docstring中的Raises中提及, 因為這樣并適合保障對于API誤用的特殊反饋
raise ValueError('Minimum port must be at least 1024, not %d.' % (minimum,))
port = self._find_next_open_port(minimum)
if not port:
raise ConnectionError('Could not connect to service on %d or higher.' % (minimum,))
assert port >= minimum, 'Unexpected port %d when minimum was %d.' % (port, minimum)
return port
No:
def connect_to_next_port(self, minimum):
"""Connects to the next available port.
Args:
minimum: A port value greater or equal to 1024.
Returns:
The new minimum port.
"""
assert minimum >= 1024, 'Minimum port must be at least 1024.'
port = self._find_next_open_port(minimum)
assert port is not None
return port
- 庫或者包可能會定義各自的異常.當這樣做的時候,必須要繼承一個已經(jīng)存在的異常類,異常類的名字應該以 Error 結(jié)尾,并且不應該引入重復(foo.FooError)
- 永遠不要用捕獲全部異常的 except:語句,或者捕獲 Exception 或者 StandardError 除非:Python 在這個方面容忍度很高,并且 except:語句會捕獲包括拼寫錯誤,sys.exit(),Ctrl+C 終止,單元測試失敗和和所有你并沒有想到捕獲的其他異常.
- 再次拋出這個異常在程序中異常不會繼續(xù)但是會被記錄以及消除(例如通過保護最外層的方式保護線程不會崩潰)的地方創(chuàng)造一個孤立點.
- 最精簡try/except表達式內(nèi)部的代碼量,try 代碼塊里的代碼體量越大,月可能會在你不希望拋出異常的代碼中拋出異常,進而在這種情況下,try/except掩蓋了一個真實的異常
- 使用finally來執(zhí)行代碼,這些代碼無論是否有異常在 try代碼塊被拋出都會被執(zhí)行.這在清理(即關(guān)閉文件)時非常有用.
- 當捕獲了異常時,用as而不是逗號分段.
try:
raise Error()
except Error as error:
pass
2.5 全局變量
避免全局變量
2.5.1 定義
在模塊級別或者作為類屬性聲明的變量
2.5.2 Pros
有些時候有用
2.5.3 Cons
在 import 的過程中,有可能改變模塊行為,因為在模塊首次被引入的過程中,全局變量就已經(jīng)被聲明
2.5.4 建議
避免全局變量
作為技術(shù)變量,模塊級別的常量是允許并鼓勵使用的.例如MAX_HOLY_HANDGRENADE_COUNT = 3, 常量必須由大寫字母和下劃線組成,參見下方命名規(guī)則
如果需要,全局變量需要在模塊級別聲明,并且通過在變量名前加_來使其對模塊內(nèi)私有化.外部對模塊全局變量的訪問必須通過公共模塊級別函數(shù),參見下方命名規(guī)則
2.6 內(nèi)嵌/局部/內(nèi)部 類和函數(shù)
內(nèi)嵌局部函數(shù)或類在關(guān)閉局部變量時是可以的.內(nèi)部類意識可用的.(譯注:這里我的理解是當內(nèi)嵌局部函數(shù)或類是和局部變量在同一個封閉作用域內(nèi)是可以的.)
2.6.1 定義
類可以在方法,函數(shù),類內(nèi)定義.函數(shù)可以在方法或函數(shù)內(nèi)定義.內(nèi)嵌函數(shù)對封閉作用域的變量具有只讀訪問權(quán)限.
2.6.2 Pros
允許定義只在非常有限作用域內(nèi)可用的工具類或工具函數(shù).Very ADT-y(??符合抽象數(shù)據(jù)類型要求???),通常用于實現(xiàn)裝飾器
2.6.3 Cons
內(nèi)嵌或局部類的實例是不能被 pickle 的,內(nèi)嵌函數(shù)或類是不能被直接測試的.嵌套會讓外部函數(shù)更長并且更難讀懂.
2.6.4 建議
除了一些特別聲明,這些內(nèi)嵌/局部/內(nèi)部類和函數(shù)都是可以的.避免內(nèi)嵌函數(shù)或類除了需要關(guān)閉一個局部值的時候.(譯者理解可能是除了將局部變量封閉在同一個作用域的情況以外).不要把一個函數(shù)轉(zhuǎn)為內(nèi)嵌指示為了避免訪問.在這種情況下,把函數(shù)置于模塊級別并在函數(shù)名前加_以保證測試是可以訪問該函數(shù)的.
2.7 列表推導和生成器表達式
在簡單情況下是可用的
2.7.1 定義
List, Dict 和 Set 推導生成式以及生成器表達式提供了一個簡明有效的方式來生成容器和迭代器而不需要傳統(tǒng)的循環(huán),map(),filter()或者lambda 表達式
2.7.2 Pros
簡單地推導表達比其他的字典,列表或集合生成方法更加簡明清晰.生成器表達式可以很有效率,因為完全避免了生成列表.
2.7.3 Cons
負載的推導表達式或生成器表達式很難讀懂
2.7.4 建議
簡單情況下使用時可以的.每個部分(mapping 表達式,filter 表達式等)都應該在一行內(nèi)完成.多個 for 條款或者 filter 表達式是不允許的.當情況變得很復雜的適合就使用循環(huán).
Yes:
result = [mapping_expr for value in iterable if filter_expr]
result = [{'key': value} for value in iterable
if a_long_filter_expression(value)]
result = [complicated_transform(x)
for x in iterable if predicate(x)]
descriptive_name = [
transform({'key': key, 'value': value}, color='black')
for key, value in generate_iterable(some_input)
if complicated_condition_is_met(key, value)
]
result = []
for x in range(10):
for y in range(5):
if x * y > 10:
result.append((x, y))
return {x: complicated_transform(x)
for x in long_generator_function(parameter)
if x is not None}
squares_generator = (x**2 for x in range(10))
unique_names = {user.name for user in users if user is not None}
eat(jelly_bean for jelly_bean in jelly_beans
if jelly_bean.color == 'black')
No:
result = [complicated_transform(
x, some_argument=x+1)
for x in iterable if predicate(x)]
result = [(x, y) for x in range(10) for y in range(5) if x * y > 10]
return ((x, y, z)
for x in range(5)
for y in range(5)
if x != y
for z in range(5)
if y != z)
2.8 默認迭代器和運算符
對支持默認迭代器和云算法的類型例如列表,字典和文件等使用它們
2.8.1 定義
容器類型(例如字典,列表等)定義了的默認的迭代器和成員檢查運算符.
Pros
默認迭代器和操作符是簡單有效的,能夠直接不需額外調(diào)用方法地表達操作.使用默認操作符的函數(shù)是通用的.能被用于任何支持這些操作的類型.
Cons
不能通過方法名來分辨類型,例如 has_key()意味著字典,當然這也是一種優(yōu)勢.
建議
對于支持的類型諸如列表,字典和文件,使用默認迭代器和操作符.內(nèi)置類型同樣定義了迭代器方法.優(yōu)先使用這些方法而非那些返回列表的方法.除非能夠確定在遍歷容器的過程中不會改變?nèi)萜?不要使用 Python 2 專有迭代方法除非必要.
Yes:
for key in adict: ...
if key not in adict: ...
if obj in alist: ...
for line in afile: ...
for k, v in adict.items(): ...
for k, v in six.iteritems(adict): ...
No:
for key in adict.keys(): ...
if not adict.has_key(key): ...
for line in afile.readlines(): ...
for k, v in dict.iteritems(): ...
2.9 生成器
需要時使用生成器
2.9.1 定義
生成器函數(shù)返回一個迭代器,每次執(zhí)行 yield 語句的時候生成一個值.在生成一個值之后,生成器函數(shù)的運行被掛起直到需要下一個值.
2.9.2 Pros
簡化代碼,因為局部變量和控制流在每次調(diào)用時被保留,生成器相比于一次性生成整個一個列表值要更節(jié)省內(nèi)存.
2.9.3 Cons
無
2.9.4 建議
建議使用.在生成器函數(shù)的文檔字符串中使用"Yields:"而非"Returns:"
2.10 Lambda表達式
單行代碼時是可以的
2.10.1 定義
lambda 在一個表達式內(nèi)定義了匿名函數(shù),而不在語句里.lambda 表達式常被用于定義高階函數(shù)(例如 map()和 filter())使用的回調(diào)函數(shù)或者操作符.
2.10.2 Pros
方便
2.10.3 Cons
比局部函數(shù)更難讀懂和 debug,匿名意味著堆棧跟蹤更難懂.表達性受限因為 lambda 函數(shù)只包含一個表達式
2.10.4 建議
對于單行代碼而言,可以使用 lambda 表達式.如果 lambda 表達式內(nèi)的代碼超過60-80個字符,最好定義成為常規(guī)的內(nèi)嵌函數(shù).
對于一般的操作諸如乘法,使用 operator 模塊內(nèi)置函數(shù)而非重新定義匿名函數(shù),例如使用 operator.mul而非lambda x,y: x * y
2.11 條件表達式
簡單情況下可以使用.
2.11.1 定義
條件表達式(也稱為三元運算符)是一種更短替代 if 語句的機制.例如x = 1 if cond else 2
2.11.2 Pros
相對于 if 語句更短也更方便
2.11.3 Cons
比if語句可能更難讀懂,當表達式很長的時候條件部分可能很難定位.
2.11.4 建議
簡單情況可以使用.每個部分(真值表達式,if表達式,else表達式)必須在一行內(nèi)完成.如果使用條件表達式很富的時候使用完整的if語句.
Yes:
one_line = 'yes' if predicate(value) else 'no'
slightly_split = ('yes' if predicate(value)
else 'no, nein, nyet')
the_longest_ternary_style_that_can_be_done = (
'yes, true, affirmative, confirmed, correct'
if predicate(value)
else 'no, false, negative, nay')
No:
bad_line_breaking = ('yes' if predicate(value) else
'no')portion_too_long = ('yes'
if some_long_module.some_long_predicate_function(
really_long_variable_name)
else 'no, false, negative, nay')
2.12 默認參數(shù)值
大多數(shù)情況下都OK
2.12.1 定義
在函數(shù)參數(shù)列表的最后可以為變量設(shè)定值,例如 def foo(a, b=0):.如果 foo 在調(diào)用時只傳入一個參數(shù),那么b變量就被設(shè)定為0,如果調(diào)用時傳入兩個參數(shù),那么b就被賦予第二個參數(shù)值.
2.12.2 Pros
通常一個函數(shù)可能會有大量默認值,但是很少會有需要修改這些默認值的時候.默認值就提供了一個很簡單滿足上述情況的方式,而不需要為這些少見的情況重新定義很多函數(shù).因為 Python 不支持重載方法或函數(shù),默認參數(shù)是一個很簡單的方式來"假重載"行為.
2.12.3 Cons
默認參數(shù)在模塊加載時就被復制.這在參數(shù)是可變對象(例如列表或字典)時引發(fā)問題.如果函數(shù)修改了這些可變對象(例如向列表尾添加元素).默認值就被改變了.
2.12.4 建議
使用時請注意以下警告----在函數(shù)或方法定義時不要將可變對象作為默認值.
Yes:
def foo(a, b=None):
if b is None:
b = []
def foo(a, b: Optional[Sequence] = None):
if b is None:
b = []
def foo(a, b: Sequence = ()): # Empty tuple OK since tuples are immutable 空元組是也不可變的
...
No:
def foo(a, b=[]):
...
def foo(a, b=time.time()): # The time the module was loaded??? 模塊被加載的時間???
...
def foo(a, b=FLAGS.my_thing): # sys.argv has not yet been parsed... sys.argv還未被解析
...
def foo(a, b: Mapping = {}): # Could still get passed to unchecked code 仍可傳入未檢查的代碼(此處翻譯可能有誤)
...
2.13 屬性
使用屬性可以通過簡單而輕量級的訪問器和設(shè)定器方法來訪問或設(shè)定數(shù)據(jù).
2.13.1 定義
一種裝飾器調(diào)用來在計算比較輕量級時作為標準的屬性訪問來獲取和設(shè)定一個屬性的方式
2.13.2 Pros
對于簡單的屬性訪問,減少顯式的 get 和 set 方法能夠提升可讀性.允許惰性計算.被認為是一種 Python 化的方式來維護類接口.在表現(xiàn)上,當直接對變量的訪問更合理時,允許屬性繞過所需的瑣碎的訪問方法.
2.13.3 Cons
在 Python2中必須繼承于 object,可能會隱藏像是操作符重載之類的副作用.對于子類而言,屬性可能有些迷惑性.
2.13.4 建議
在通常會有簡單而且輕量級的訪問和設(shè)定方法的新代碼里使用屬性來訪問或設(shè)定數(shù)據(jù).屬性在創(chuàng)建時被@property 裝飾,參加裝飾器
如果屬性本身未被重寫,帶有屬性的繼承可能不夠明晰,因而必須確保訪問方法是被間接訪問的,來確保子類的方法重載是被屬性調(diào)用的(使用 Template Method DP,譯者:應是模板方法設(shè)計模式).
Yes:
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
@property
def area(self):
"""Area of the square."""
return self._get_area()
@area.setter
def area(self, area):
return self._set_area(area)
def _get_area(self):
"""Indirect accessor to calculate the 'area' property."""
return self.side ** 2
def _set_area(self, area):
"""Indirect setter to set the 'area' property."""
self.side = math.sqrt(area)
@property
def perimeter(self):
return self.side * 4
2.14 True/False表達式
只要可能,就使用隱式False的if語句
2.14.1 定義
在布爾環(huán)境下,Python 對某些值判定為False,一個快速的經(jīng)驗規(guī)律是所有"空"值都被認為是 False,所以0, None, [], {}, ''的布爾值都是 False
2.14.2 Pros
使用 Python 布爾類型的條件語句可讀性更好而且更難出錯,大多數(shù)情況下,這種方式也更快.
2.14.3 Cons
對于 C/C++開發(fā)者而言可能有些奇怪
建議
如果可能的話,使用隱式 False.例如使用 if foo:而非 if foo != []:下面列舉了一些你應該牢記的警告:
- 使用 if foo is None(或者 if foo is not None)來檢查None.例如在檢查一個默認值是 None 的變量或者參數(shù)是否被賦予了其他值的時候,被賦予的其他值的布爾值可能為False.
- 不要用==來和布爾值為 False 的變量比較,使用 if not x,如果需要區(qū)別 False 和 None,那么使用鏈式的表達式如if not x and x is not None
- 對于序列(如字符串,列表,元組),利用空序列為 False 的事實,故而相應地使用 if seq:和 if not seq:而非 if len(seq)或 if not len(seq):.
- 在處理整數(shù)時,隱式的 False 可能會引入更多風險(例如意外地將 None 和 0進行了相同的處理)你可以用一個已知是整形(并且不是len()的結(jié)果)的值和整數(shù)0比較.
Yes:
if not users:
print('no users')
if foo == 0:
self.handle_zero()
if i % 10 == 0:
self.handle_multiple_of_ten()
def f(x=None):
if x is None:
x = []
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()
def f(x=None):
x = x or []
2.15 棄用的語言特性
盡可能利用字符串方法而非 string 模塊.使用函數(shù)調(diào)用語法而非 apply.在函數(shù)參數(shù)本就是一個行內(nèi)匿名函數(shù)的時候,使用列表推導表達式和 for 循環(huán)而非 filter 和 map
2.15.1 定義
當前 Python 版本提供了人們普遍更傾向的構(gòu)建方式.
2.15.2 建議
我們不使用任何不支持這些特性的 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. 可以,沒有行內(nèi)的lambda表達式
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)
2.16 詞法作用域
可以使用
2.16.1 定義
一個內(nèi)嵌 Python 函數(shù)可以引用在閉包命名空間內(nèi)定義的變量,但是不能對其復制.變量綁定是解析到使用詞法作用域的,即基于靜態(tài)程序文本.任何對塊內(nèi)命名的賦值都會讓 Python 將對于這個命名的引用都作為局部變量,即使在使用先于賦值的情況下也是.如果有全局聲明,這個命名就會被認為是全局變量.
一個使用這個特性的例子是:
def get_adder(summand1):
"""Returns a function that adds numbers to a given number."""
def adder(summand2):
return summand1 + summand2
return adder
2.16.2 Pros
經(jīng)??梢宰尨a更簡明優(yōu)雅,尤其會讓有經(jīng)驗的 Lisp 和 Scheme(以及 Haskell 和 ML 還有其他)的程序要很舒服.
2.16.3 Cons
可能會導致令人迷惑的 bug 例如這個基于PEP-0227的例子.
i = 4
def foo(x):
def bar():
print(i, end='')
# ...
# A bunch of code here
# ...
for i in x: # Ah, i *is* local to foo, so this is what bar sees i對于foo來說是局部變量,所以在這里就是bar函數(shù)所獲取的值
print(i, end='')
bar()
所以foo([1, 2, 3])會打印1 2 3 3而非1 2 3 4.
2.16.4 建議
可以使用
2.17 函數(shù)和方法裝飾器
在明顯有好處時,謹慎明智的使用,避免@staticmethod,控制使用@classmethod
2.17.1 定義
函數(shù)和方法裝飾器(也就是@記號).一個常見的裝飾器是@property,用于將普通方法轉(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)
2.17.2 Pros
能夠優(yōu)雅的對方法進行某種轉(zhuǎn)換,而該轉(zhuǎn)換可能減少一些重復代碼并保持不變性等等.
2.17.3 Cons
裝飾器可以對函數(shù)的參數(shù)和返回值任意操作,導致非常隱形的操作行為.此外,裝飾器在 import 的時候就被執(zhí)行,裝飾器代碼的實效可能非常難恢復.
2.17.4 建議
在有明顯好處的地方謹慎地使用裝飾器.裝飾器應該和函數(shù)遵守相同的 import 和命名指導規(guī)則.裝飾器的文檔應該清晰地聲明該函數(shù)為裝飾器函數(shù).并且要為裝飾器函數(shù)編寫單元測試.
避免裝飾器自身對外部的依賴,(如不要依賴于文件,socket,數(shù)據(jù)庫連接等等),這是由于在裝飾器運行的時候(在 import 時,可能從 pydoc 或其他工具中)這些外部依賴可能不可用.一個被傳入有效參數(shù)并調(diào)用的裝飾器應該(盡可能)保證在任何情況下都可用.
裝飾器是一種特殊的"頂級代碼",參見main
永遠不要使用@staticmethod,除非不得不整合一個 API 到一個已有的庫,應該寫一個模塊等級的函數(shù).
只在寫一個命名的構(gòu)造器或者一個類特定的,修改必要的全局狀態(tài)(例如進程緩存等)的流程時使用@classmethod.
2.18 線程
不要依賴于內(nèi)建類型的原子性
盡管 Python 內(nèi)置數(shù)據(jù)類型例如字典等似乎有原子性操作,仍有一些罕見情況下,他們是非原子的(比如,如果__hash__或者__eq__被實現(xiàn)為Python方法),就不應該依賴于這些類型的原子性.也不應該依賴于原子變量賦值(因為這依賴于字典)
優(yōu)先使用 Queue 模塊的 Queue 類來作為線程之間通訊數(shù)據(jù)的方式.此外,要是用 threading 模塊和其 locking primitives(鎖原語).了解條件變量的合理用法以便于使用 threading.Condition 而非使用更低級的鎖.
2.19 過于強大的特性
盡量避免使用
2.19.1 定義
Python 是一種非常靈活的語言并且提供了很多新奇的特性,諸如定制元類,訪問字節(jié)碼,動態(tài)編譯,動態(tài)繼承,對象父類重定義,import hacks,反射(例如一些對于getattr()的應用),系統(tǒng)內(nèi)置的修改等等.
2.19.2 Pros
這些是非常強大的語言特性,可以讓程序更緊湊
2.19.3 Cons
使用這些新特性是很誘人的.但是并不絕對必要,它們很難讀很難理解.也很難 debug 那些在底層使用了不常見的特性的代碼.對于原作者而言可能不是這樣,但是再次看代碼的時候,可能比更長但是更直接的代碼要難.
2.19.4 定義
避免在代碼中使用這些特性.
內(nèi)部使用這些特性的標準庫和類是可以使用的(例如 abc.ABCMeta,collections.namedtuple,和enum)
2.20 新版本Python: Python3 和從__future__import
Python3已經(jīng)可用了(譯者:目前 Python2已經(jīng)不受支持了),盡管不是每個項目都準備好使用 Python3,所有的代碼應該兼容 Python3并且在可能的情況下在 Python3的環(huán)境下測試.
2.20.1 定義
Python3是 Python的重大改變,盡管現(xiàn)有代碼通常是 Python2.7寫成的,但可以做一些簡單的事情來讓代碼更加明確地表達其意圖,從而可以讓代碼更好地在 Python3下運行而不用調(diào)整.
2.20.2 Pros
在考慮 Python3編寫的代碼更清晰明確,一旦所有依賴已就緒,就可以更容易在 Python3環(huán)境下運行.
2.20.3 Cons
一些人會認為默認樣板有些丑,import實際不需要的特性到模塊中是不常見的.
2.20.4 建議
from future imports
鼓勵使用from __future__ import語句.所有新代碼都應該包含下述代碼,而現(xiàn)有代碼應該被更新以盡可能兼容:
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
如果你不太熟悉這些,詳細閱讀這些:絕對import,新的/除法行為,和print函數(shù)
請勿省略或移除這些import,即使在模塊中他們沒有在使用,除非代碼只用于Python3.最好總是在所有的文檔中都有從future的import,來保證不會在有人使用在后續(xù)編輯時遺忘.
有其他的from __future__import語句,看喜好使用.我們的建議中不包含unicode_literals因為其并無明顯優(yōu)勢,這是由于隱式默認的編碼轉(zhuǎn)換導致其在Python2.7內(nèi)很多地方被引入了,必要時,大多數(shù)代碼最好顯式的使用b''和u''btyes和unicode字符串表示.(譯者:這段翻譯可能不準確)
The six, future, or past libraries
當項目需要支持Python2和3時,根據(jù)需求使用six,future和past.
2.21 帶有類型注釋的代碼
可以根據(jù)PEP-484對Python3代碼進行類型注釋,并且在build時用類型檢查工具例如pytype進行類型檢查.
類型注釋可以在源碼中或stub pyi file中.只要可能,注釋就應寫在源代碼中.對于第三方或拓展模塊使用pyi文件.
2.21.1 定義
類型注釋(也稱為"類型提示")是用于函數(shù)或方法參數(shù)和返回值的:
def func(a: int) -> List[int]:
你也可以聲明用一個單獨的注釋來聲明變量的類型:
a = SomeFunc() # type: SomeType
2.21.2 Pros
類型注釋提升代碼的可讀性和可維護性,類型檢查會將很多運行錯誤轉(zhuǎn)化為構(gòu)建錯誤,也減少了使用過于強力特性的能力.
2.21.3 Cons
需要不斷更新類型聲明,對于認為有效的代碼可能會報類型錯誤,使用類型檢查可能減少使用過于強力特性的能力.
2.21.4 建議
強烈鼓勵在更新代碼的時候進行Python類型分析.在對公共API進行補充和修改時,包括python類型聲明并通過構(gòu)建系統(tǒng)中的pytype進行檢查.對Python來說靜態(tài)類型檢查比較新,我們承認,一些意料外的副作用(例如錯誤推斷的類型)可能拒絕一些項目的使用.這種情況下,鼓勵作者適當?shù)卦黾右粋€帶有TODO或到bug描述當前不接搜的類型注釋的鏈接到BUILD文件或者在代碼內(nèi).
3 Python代碼風格規(guī)范
3.1 分號
不要在行尾加分號,也不要用分號把兩行語句合并到一行
3.2 行長度
最大行長度是80個字符
超出80字符的明確例外:
- 長import
- 注釋中的:URL,路徑,flags等
- 不包含空格不方便分行的模塊級別的長字符串常量
- pylint的diable注釋使用(如# pylint: disable=invalid-name)
不要使用反斜杠連接,除非對于需要三層或以上的上下文管理器with語句
利用Python的implicit line joining inside parentheses, brackets and braces(隱式行連接方法--括號連接,包括(), [], {}).如果必要的話,也可在表達式外面額外添加一對括號.
Yes:
foo_bar(self, width, height, color='black', design=None, x='foo',
emphasis=None, highlight=0)
if (width == 0 and height == 0 and
color == 'red' and emphasis == 'strong'):
當字符串不能在一行內(nèi)完成時,使用括號來隱式連接行:
x = ('This will build a very long long '
'long long long long long long string')
在注釋內(nèi),如有必要,將長URL放在其本行內(nèi):
Yes:
# See details at
# http://www.example.com/us/developer/documentation/api/content/v2.0/csv_file_name_extension_full_specification.html
No:
# See details at
# http://www.example.com/us/developer/documentation/api/content/\
# v2.0/csv_file_name_extension_full_specification.html
在定義一個表達式超過三行或更多的with語句時,可以使用反斜杠來分行.對于兩行表達式,使用嵌套with語句:
Yes:
with very_long_first_expression_function() as spam, \
very_long_second_expression_function() as beans, \
third_thing() as eggs:
place_order(eggs, beans, spam, beans)
with very_long_first_expression_function() as spam:
with very_long_second_expression_function() as beans:
place_order(beans, spam)
No:
with VeryLongFirstExpressionFunction() as spam, \
VeryLongSecondExpressionFunction() as beans:
PlaceOrder(eggs, beans, spam, beans)
注意上述例子中的縮進,具體參看縮進
在其他一行超過80字符的情況下,而且yapf自動格式工具也不能使分行符合要求時,允許超過80字符限制.
3.3 括號
括號合理使用
盡管不必要,但是可以在元組外加括號.再返回語句或者條件語句中不要使用括號,除非是用于隱式的連接行或者指示元組.
Yes:
if foo:
bar()
while x:
x = bar()
if x and y:
bar()
if not x:
bar()
# For a 1 item tuple the ()s are more visually obvious than the comma.
onesie = (foo,)
return foo
return spam, beans
return (spam, beans)
for (x, y) in dict.items(): ...
No:
if (x):
bar()
if not(x):
bar()
return (foo)
3.4 縮進
縮進用4個空格
縮進代碼段不要使用制表符,或者混用制表符和空格.如果連接多行,多行應垂直對齊,或者再次4空格縮進(這個情況下首行括號后應該不包含代碼).
Yes:
# Aligned with opening delimiter
# 和opening delimiter對齊(譯者理解是分隔符的入口,例如三種括號,字符串引號等)
foo = long_function_name(var_one, var_two,
var_three, var_four)
meal = (spam,
beans)
# Aligned with opening delimiter in a dictionary
foo = {
long_dictionary_key: value1 +
value2,
...
}
# 4-space hanging indent; nothing on first line
# 縮進4個空格,首行括號后無內(nèi)容
foo = long_function_name(
var_one, var_two, var_three,
var_four)
meal = (
spam,
beans)
# 4-space hanging indent in a dictionary
foo = {
long_dictionary_key:
long_dictionary_value,
...
}
No:
# Stuff on first line forbidden
# 首行不允許有內(nèi)容
foo = long_function_name(var_one, var_two,
var_three, var_four)
meal = (spam,
beans)
# 2-space hanging indent forbidden
foo = long_function_name(
var_one, var_two, var_three,
var_four)
# No hanging indent in a dictionary
foo = {
long_dictionary_key:
long_dictionary_value,
...
}
3.4.1 關(guān)于尾后逗號
關(guān)于在一序列元素中的尾號逗號,只推薦在容器結(jié)束符號],)或者}和最后元素不在同一行時使用.尾后逗號的存在也被用作我們Python代碼自動格式化工具yapf的提示,在,最后元素之后出現(xiàn)的時候來自動調(diào)整容器元素到每行一個元素.
Yes:
golomb3 = [0, 1, 3]
golomb4 = [
0,
1,
4,
6,
]
No:
golomb4 = [
0,
1,
4,
6
]
3.5 空行
在頂級定義(函數(shù)或類)之間要間隔兩行.在方法定義之間以及class所在行與第一個方法之間要空一行,def行后無空行,在函數(shù)或方法內(nèi)你認為合適地方可以使用單空行.
3.6 空格
遵守標準的空格和標點排版規(guī)則.
括號(),[],{}內(nèi)部不要多余的空格.
Yes:
spam(ham[1], {eggs: 2}, [])
No:
spam( ham[ 1 ], { eggs: 2 }, [ ] )
逗號、分號、冒號前不要空格,但是在后面要加空格,除非是在行尾.
Yes:
if x == 4:
print(x, y)
x, y = y, x
No:
if x == 4 :
print(x , y)
x , y = y , x
在函數(shù)調(diào)用括號的前,索引切片括號前都不加空格.
Yes:
spam(1)
dict['key'] = list[index]
No:
spam (1)
dict ['key'] = list [index]
行尾不要加空格.
在賦值(=),比較(==,<,>,!=,<>,<=,>=,in,not in,is,is not),布爾符號(and,or,not)前后都加空格.視情況在算術(shù)運算符(+,-,*,/,//,%,**,@),前后加空格
Yes:
x == 1
No:
x<1
在關(guān)鍵字名參數(shù)傳遞或定義默認參數(shù)值的時候不要在=前后加空格,只有一個例外:當類型注釋存在時在定義默認參數(shù)值時=前后加空格
Yes:
def complex(real, imag=0.0): return Magic(r=real, i=imag)
def complex(real, imag: float = 0.0): return Magic(r=real, i=imag)
No:
def complex(real, imag = 0.0): return Magic(r = real, i = imag)
def complex(real, imag: float=0.0): return Magic(r = real, i = imag)
不要用空格來做無必要的對齊,因為這會在維護時帶來不必要的負擔(對于:.#,=等等).
Yes:
foo = 1000 # comment
long_name = 2 # comment that should not be aligned
dictionary = {
'foo': 1,
'long_name': 2,
}
No:
foo = 1000 # comment
long_name = 2 # comment that should not be aligned
dictionary = {
'foo' : 1,
'long_name': 2,
}
3.7 Shebang
大部分.py文件不需要從#!行來開始.根據(jù)PEP-394,程序的主文件應該以#!/usr/bin/python2或#!/usr/bin/python3起始
這行被用于幫助內(nèi)核找到Python解釋器,但是在導入模塊時會被Python忽略/只在會被直接運行的文件里有必要寫.
3.8 注釋和文檔字符串
確保使用正確的模塊,函數(shù),方法的文檔字符串和行內(nèi)注釋.
3.8.1 文檔字符串
Python使用文檔字符串來為代碼生成文檔.文檔字符串是包,模塊,類或函數(shù)的首個語句.這些字符串能夠自動被__doc__成員方法提取并且被pydoc使用.(嘗試在你的模塊上運行pydoc來看看具體是什么).文檔字符串使用三重雙引號"""(根據(jù)PEP-257).文檔字符串應該這樣組織:一行總結(jié)(或整個文檔字符串只有一行)并以句號,問好或感嘆號結(jié)尾.隨后是一行空行,隨后是文檔字符串,并與第一行的首個引號位置相對齊.更多具體格式規(guī)范如下.
3.8.2 模塊
每個文件都應包含許可模板.選擇合適的許可模板用于項目(例如Apache 2.0,BSD,LGPL,GPL)
文檔應該以文檔字符串開頭,并描述模塊的內(nèi)容和使用方法.
"""A one line summary of the module or program, terminated by a period.
Leave one blank line. The rest of this docstring should contain an
overall description of the module or program. Optionally, it may also
contain a brief description of exported classes and functions and/or usage
examples.
Typical usage example:
foo = ClassFoo()
bar = foo.FunctionBar()
"""
3.8.3 函數(shù)和方法
在本節(jié),"函數(shù)"所指包括方法,函數(shù)或者生成器.
函數(shù)應有文檔字符串,除非符合以下所有條件:
- 外部不可見
- 非常短
- 簡明
文檔字符串應該包含足夠的信息以在無需閱讀函數(shù)代碼的情況下調(diào)用函數(shù).文檔字符串應該是敘事體("""Fetches rows from a Bigtable.""")的而非命令式的("""Fetch rows from a Bigtable."""),除了@property(應與attribute使用同樣的風格).文檔字符串應描述函數(shù)的調(diào)用語法和其意義,而非實現(xiàn).對比較有技巧的地方,在代碼中使用注釋更合適.
覆寫了基類的方法可有簡單的文檔字符串向讀者指示被覆寫方法的文檔字符串例如"""See base class.""".這是因為沒必要在很多地方重復已經(jīng)在基類的文檔字符串中存在的文檔.不過如果覆寫的方法行為實際上與被覆寫方法不一致,或者需要提供細節(jié)(例如文檔中表明額外的副作用),覆寫方法的文檔字符串至少要提供這些差別.
一個函數(shù)的不同方面應該在特定對應的分節(jié)里寫入文檔,這些分節(jié)如下.每一節(jié)都由以冒號結(jié)尾的一行開始, 每一節(jié)除了首行外,都應該以2或4個空格縮進并在整個文檔內(nèi)保持一致(譯者建議4個空格以維持整體一致).如果函數(shù)名和簽名足夠給出足夠信息并且能夠剛好被一行文檔字符串所描述,那么可以忽略這些節(jié).
Args:
列出每個參數(shù)的名字.名字后應有為冒號和空格,后跟描述.如果描述太長不能夠在80字符的單行內(nèi)完成.那么分行并縮進2或4個空格且與全文檔一致(譯者同樣建議4個空格)
描述應該包含參數(shù)所要求的類型,如果代碼不包含類型注釋的話.如果函數(shù)容許*foo(不定長度參數(shù)列表)或**bar(任意關(guān)鍵字參數(shù)).那么就應該在文檔字符串中列舉為*foo和**bar.
Returns:(或?qū)τ谏善魇荵ields:)
描述返回值的類型和含義.如果函數(shù)至少返回None,這一小節(jié)不需要.如果文檔字符串以Returns或者Yields開頭(例如"""Returns row from Bigtable as a tuple of strings.""")或首句足夠描述返回值的情況下這一節(jié)可忽略.
Raises:
列出所有和接口相關(guān)的異常.對于違反文檔要求而拋出的異常不應列出.(因為這會矛盾地使得違反接口要求的行為成為接口的一部分)
def fetch_bigtable_rows(big_table, keys, other_silly_variable=None):
"""Fetches rows from a Bigtable.
Retrieves rows pertaining to the given keys from the Table instance
represented by big_table. Silly things may happen if
other_silly_variable is not None.
Args:
big_table: An open Bigtable Table instance.
keys: A sequence of strings representing the key of each table row
to fetch.
other_silly_variable: Another optional variable, that has a much
longer name than the other args, and which does nothing.
Returns:
A dict mapping keys to the corresponding table row data
fetched. Each row is represented as a tuple of strings. For
example:
{'Serak': ('Rigel VII', 'Preparer'),
'Zim': ('Irk', 'Invader'),
'Lrrr': ('Omicron Persei 8', 'Emperor')}
If a key from the keys argument is missing from the dictionary,
then that row was not found in the table.
Raises:
IOError: An error occurred accessing the bigtable.Table object.
"""
3.8.4 類
類定義下一行應為描述這個類的文檔字符串.如果類有公共屬性,應該在文檔字符串中的Attributes節(jié)中注明,并且和函數(shù)的Args一節(jié)風格統(tǒng)一.
class SampleClass(object):
"""Summary of class here.
Longer class information....
Longer class information....
Attributes:
likes_spam: A boolean indicating if we like SPAM or not.
eggs: An integer count of the eggs we have laid.
"""
def __init__(self, likes_spam=False):
"""Inits SampleClass with blah."""
self.likes_spam = likes_spam
self.eggs = 0
def public_method(self):
"""Performs operation blah."""
3.8.5 塊注釋和行注釋
最后要在代碼中注釋的地方是代碼技巧性的部分.如果你將要在下次code review中揭示代碼.應該現(xiàn)在就添加注釋.在復雜操作開始前,注釋幾行.對于不夠明晰的代碼在行尾注釋.
# We use a weighted dictionary search to find out where i is in
# the array. We extrapolate position based on the largest num
# in the array and the array size and then do binary search to
# get the exact number.
if i & (i-1) == 0: # True if i is 0 or a power of 2.
為了提升易讀性,行注釋應該至少在代碼2個空格后,并以#后接至少1個空格開始注釋部分.
另外,不要描述代碼,假定閱讀代碼的人比你更精通Python(他只是不知道你試圖做什么).
3.8.6 標點,拼寫和語法
注意標點,拼寫和語法,寫得好的注釋要比寫得差的好讀.
注釋應當是和敘事性文本一樣可讀,并具有合適的大小寫和標點.在許多情況下,完整的句子要比破碎的句子更可讀.更簡短的注釋如行尾的注釋有時會不太正式,但是應該全篇保持風格一致.
盡管被代碼審核人員指出在應該使用分號的地方使用了逗號是很令人沮喪的,將源代碼維護在高度清楚可讀的程度是很重要的.合適的標點,拼寫和語法能夠幫助達到這個目標.
3.9 類
如果類并非從其他基類繼承而來,那么就要明確是從object繼承而來,即便內(nèi)嵌類也是如此.
Yes:
class SampleClass(object):
pass
class OuterClass(object):
class InnerClass(object):
pass
class ChildClass(ParentClass):
"""Explicitly inherits from another class already."""
No:
class SampleClass:
pass
class OuterClass:
class InnerClass:
pass
從object類繼承保證了屬性能夠在Python2正確運行并且保護代碼在Python3下出現(xiàn)潛在的不兼容.這樣也定義了object包括__new__,__init__,__delattr__,__getattribute__,__setattr__,__hash__,__repr__,和__str__等默認特殊方法的實現(xiàn).
3.10 字符串
使用format或%來格式化字符串,即使參數(shù)都是字符串對象,也要考慮使用+還是%及format.
Yes:
x = a + b
x = '%s, %s!' % (imperative, expletive)
x = '{}, {}'.format(first, second)
x = 'name: %s; score: %d' % (name, n)
x = 'name: {}; score: {}'.format(name, n)
x = f'name: {name}; score: {n}' # Python 3.6+
No:
employee_table = '<table>'
for last_name, first_name in employee_list:
employee_table += '<tr><td>%s, %s</td></tr>' % (last_name, first_name)
employee_table += '</table>'
避免使用+和+=操作符來在循環(huán)內(nèi)累加字符串,因為字符串是不可變對象.這會造成不必要的臨時變量導致運行時間以四次方增長而非線性增長.應將每個字符串都記入一個列表并使用''.join來將列表在循環(huán)結(jié)束后連接(或?qū)⒚總€子字符串寫入io.BytesIO緩存)
Yes:
items = ['<table>']
for last_name, first_name in employee_list:
items.append('<tr><td>%s, %s</td></tr>' % (last_name, first_name))
items.append('</table>')
employee_table = ''.join(items)
No:
employee_table = '<table>'
for last_name, first_name in employee_list:
employee_table += '<tr><td>%s, %s</td></tr>' % (last_name, first_name)
employee_table += '</table>'
在同一個文件內(nèi),字符串引號要一致,選擇''或者""并且不要改變.對于需要避免\\轉(zhuǎn)義的時候,可以更改.
Yes:
Python('Why are you hiding your eyes?')
Gollum("I'm scared of lint errors.")
Narrator('"Good!" thought a happy Python reviewer.')
No:
Python("Why are you hiding your eyes?")
Gollum('The lint. It burns. It burns us.')
Gollum("Always the great lint. Watching. Watching.")
多行字符串多行字符串優(yōu)先使用"""而非''',當且只當對所有非文檔字符串的多行字符串都是用'''而且對正常字符串都使用'時才可使用三單引號.docstring不論如何必須使用"""
多行字符串和其余代碼的縮進方式不一致.如果需要避免在字符串中插入額外的空格,要么使用單行字符串連接或者帶有textwarp.dedent()的多行字符串來移除每行的起始空格.
No:
long_string = """This is pretty ugly.
Don't do this.
"""
Yes:
long_string = """This is fine if your use case can accept
extraneous leading spaces."""
long_string = ("And this is fine if you can not accept\n" +
"extraneous leading spaces.")
long_string = ("And this too is fine if you can not accept\n"
"extraneous leading spaces.")
import textwrap
long_string = textwrap.dedent("""\
This is also fine, because textwrap.dedent()
will collapse common leading spaces in each line.""")