Python程式碼風格指南&程式設計規範
1 前言
紛吾既有此內美兮,又重之以修能。 ---- 屈原《離騷》
1.1 編寫目的
本文用於指導我司使用python語言進行開發的人員。
1.2 範圍
測試人員、開發人員以及其他python語言使用者。
2 簡介
很多語言都有自己獨有的編碼風格,python以簡潔優美著稱,當然也不能例外。如果和本文規則發生任何衝突,請優先與專案級別的程式碼風格保持一致。程式碼風格一致性當然重要,想象一下空姐的制服誘惑,是不是賞心悅目呢?但也要有自己的主觀判斷,不墨守陳規,愚蠢的追求程式碼一致性,例如有以下的場景:
1、遵循此風格寫的一小片程式碼看起來和專案內其他程式碼格格不入,看到就想摳出來餵豬,人人見而曰日之。
2、遵循此風格後和其他 python 版本不相容,甚至出現錯誤,這就尷尬了。
3、遵循此風格中的某些條目使程式碼更不易讀,簡單說就是醜。
3 程式碼佈局
3.1 縮排
一個縮排級別四個空格。
1. 連續行使用兩種方式使封裝元素成為一行:括號內垂直隱式連線 & 懸掛式縮排。 使用懸掛式縮排應該注意第一行不應該有引數,連續行要使用進一步的縮排來區分。
是: # 括號內隱式連線,垂直對齊 foo = long_function_name(var_one, var_two, var_three, var_four) # 懸掛縮排,進一步縮排區分其他語句def long_function_name( var_one, var_two, var_three, var_four): print(var_one) # 懸掛縮排,一般是四個空格,但非必須 foo = long_function_name( var_one, var_two, var_three, var_four) 否: # 括號內隱式連線,沒有垂直對齊時,第一行的引數被禁止 foo = long_function_name(var_one, var_two, var_three, var_four) # 懸掛縮排,需要進一步的縮排區分其他行def long_function_name( var_one, var_two, var_three, var_four): print(var_one)
2.當 if 語句過長時,可選的處理方式,但不限於此:
# 不使用額外縮排if (this_is_one_thing and
that_is_another_thing):
do_something()
# 增加註釋區分,支援語法高亮if (this_is_one_thing and
that_is_another_thing):
# Since both conditions are true, we can frobnicate.
do_something()
# 條件連續行額外縮排if (this_is_one_thing
and that_is_another_thing):
do_something()
3.當閉環括號內元素跨行時,可以採用以下方式。
my_list = [
1, 2, 3,
4, 5, 6,
]
result = some_function_that_takes_arguments(
'a', 'b', 'c',
'd', 'e', 'f',
)
或者
my_list = [
1, 2, 3,
4, 5, 6,
]
result = some_function_that_takes_arguments(
'a', 'b', 'c',
'd', 'e', 'f',
)
3.2 空格和tab
Python 3 不允許 tab 和 space 混用,同時混用了 tab 和 space 的 Python 2 程式碼應該被轉換為僅使用 space。
3.3 語句中的空格
在以下場景避免不必要的空格:
是: spam(ham[1], {eggs: 2})
否: spam( ham[ 1 ], { eggs: 2 } )
是: foo = (0,)
否: bar = (0, )
是: if x == 4: print x, y; x, y = y, x
否: if x == 4 : print x , y ; x , y = y , x
是:
ham[1:9], ham[1:9:3], ham[:9:3], ham[1::3], ham[1:9:]
ham[lower:upper], ham[lower:upper:], ham[lower::step]
ham[lower+offset : upper+offset]
ham[: upper_fn(x) : step_fn(x)], ham[:: step_fn(x)]
ham[lower + offset : upper + offset]
否:
ham[lower + offset:upper + offset]
ham[1: 9], ham[1 :9], ham[1:9 :3]
ham[lower::upper]
ham[:upper]
是: spam(1)
否: spam (1)
是: dct['key'] = lst[index]
否: dct ['key'] = lst [index]
是:
x = 1
y = 2
long_variable = 3
否:
x = 1
y = 2
long_variable = 3
在任何地方避免使用尾隨空格。
在二元運算子周圍使用空格:
是:
i = i + 1
submitted += 1
x = x*2 - 1
hypot2 = x*x + y*y
c = (a+b) * (a-b)
否:
i=i+1
submitted +=1
x = x * 2 - 1
hypot2 = x * x + y * y
c = (a + b) * (a - b)
表示關鍵字引數或預設引數值時,不要使用空格:
是:
def complex(real, imag=0.0):
return magic(r=real, i=imag)
否:
def complex(real, imag = 0.0):
return magic(r = real, i = imag)
函式註解的場景:
是:
def munge(input: AnyStr): ...
def munge() -> AnyStr: ...
否:
def munge(input:AnyStr): ...
def munge()->PosInt: ...
當引數註釋和預設值共存時:
是:
def munge(sep: AnyStr = None): ...
def munge(input: AnyStr, sep: AnyStr = None, limit=1000): ...
否:
def munge(input: AnyStr=None): ...
def munge(input: AnyStr, limit = 1000): ...
同行多語句不建議使用:
是:
if foo == 'blah':
do_blah_thing()
do_one()
do_two()
do_three()
否:
if foo == 'blah': do_blah_thing()
do_one(); do_two(); do_three()
下面這種醜就不多說了:
否:
if foo == 'blah': do_blah_thing()
for x in lst: total += x
while t < 10: t = delay()
否:
if foo == 'blah': do_blah_thing()
else: do_non_blah_thing()
try: something()
finally: cleanup()
do_one(); do_two(); do_three(long, argument,
list, like, this)
if foo == 'blah': one(); two(); three()
3.4 程式碼行最大長度
將所有行限制為最多79個字元。對於具有較少結構限制(文件字串或註釋)的長文字塊,行長度應限制為72個字元。當然了,不要問為啥非得是 7972。因為要兼顧非洲大陸人民的生活。程式碼是全世界的。反斜槓有時可能仍然要用。 例如,又多又長的 with - 語句不能使用隱式連線,這時反斜槓是可以接受的:
with open('/path/to/some/file/you/want/to/read') as file_1, \
open('/path/to/some/file/being/written', 'w') as file_2:
file_2.write(file_1.read())
assert 語句也是如此。
3.5 運算子前還是後斷行?
演算法和程式設計技術先驅,計算機排版系統 TEX 和 METAFONT 的發明者 Donald Knuth,推薦使用以下形式:
# 是: easy to match operators with operands
income = (gross_wages
+ taxable_interest
+ (dividends - qualified_dividends)
- ira_deduction
- student_loan_interest)
只要保持本地一致性,在二元運算子之前和之後斷開都是允許的,但是新的 Python 程式碼推薦使用 Knuth 形式。
3.6 空行
頂層函式和類定義間使用兩個空行。
類內方法定義間使用一個空行。
不同函式組之間使用兩個空行隔離。
總之,空行的作用就是隔離不同函式類等,使層次分明。
3.7 原始檔編碼
Python 2 預設ASCII,Python 3 預設UTF-8。使用 ASCII 的 Python 2 原始檔或使用 UTF-8 的 Python 3 原始檔不應該有編碼宣告。原始檔最好只使用 ASCII 字元,即使是蹩腳的 Chinglish 亦可,家和萬事興。
3.8 模組匯入
是:
from subprocess import Popen, PIPE
import os
import sys
否:
import sys, os
模組匯入總是位於檔案頂部,在模組註釋和文件字串之後,模組全域性變數和常量之前。
匯入應該按照以下順序分組,不同組間用空行隔離。
- 標準庫 imports
- 相關第三方 imports
- 本地特定應用/庫 imports
推薦使用絕對匯入,標準庫程式碼應總是使用絕對匯入。
import mypkg.sibling
from mypkg import sibling
from mypkg.sibling import example
在包結構比較複雜時,可以使用相對匯入。
from . import sibling
from .sibling import example
在 Python 3 中,相對匯入已經被刪除,禁止使用。類匯入:
from myclass import MyClass
from foo.bar.yourclass import YourClass
如果這種方式導致了本地命名衝突,可以使用以下方式:
import myclass
import foo.bar.yourclass
然後使用 myclass.MyClass 和 foo.bar.yourclass.YourClass。
請不要使用以下方式:
from <module> import *
3.9 模組級別 dunder 名稱
模組級別 “dunders”(即具有兩個前導和兩個字尾下劃線的名稱),例如 __all__,__author__,__version__ 等應放在模組 docstring 之後,但在任何 import 語句之前,但是除了 __future__ 匯入。 Python 強制 future-imports 必須在除了 docstrings 之外的任何其他程式碼之前出現在模組中。
例如:
"""This is the example module.This module does stuff."""
from __future__ import barry_as_FLUFL
__all__ = ['a', 'b', 'c']__version__ = '0.1'
__author__ = 'Cardinal Biggles'
import os
import sys
4 字串引號,逗號
4.1 字串引號
在 Python 中,單引號和雙引號是等價的,只需要堅持使用一種並保持一致即可。
在雙引號中使用單引號,單引號中使用雙引號。三引號中使用雙引號。
4.2 逗號
單元素元組強制使用逗號:
是:
FILES = ('setup.cfg',)
OK, but confusing:
FILES = 'setup.cfg',
當使用版本控制系統時,一組希望後續擴充套件的值/引數/改善的條目使用以下形式:
是:
FILES = [
'setup.cfg',
'tox.ini',
]
initialize(FILES,
error=True,
)
否:
FILES = ['setup.cfg', 'tox.ini',]
initialize(FILES, error=True,)
5 註釋
糟糕的註釋不如沒有註釋,最好要用 English 註釋!
5.1 塊註釋
同等級別的一塊程式碼的註釋,塊註釋內每行註釋以 # 開頭,內部註釋段落之間使用以 # 開頭的空行註釋隔開。
5.2 行註釋
行註釋和程式碼宣告間至少間隔兩個空格,不要使用無聊的行註釋,例如:
Don't do this:
x = x + 1 # Increment x
But sometimes, this is useful:
x = x + 1 # Compensate for border
5.3 文件字串
為所有公共模組,函式,類和方法編寫文件字串。 對於非公共方法,文字字串不是必需的,但應該有一個描述該方法的註釋。例如:
"""Return a foobangOptional plotz says to frobnicate the bizbaz first."""
"""only one single docstring line"""
注意當註釋為多行時,最終的 """ 單獨起一行。
6 命名約定
對使用者可見的公共 API 部分的命名應該遵從反應如何使用而不是怎麼實現。
6.1 描述性: 命名風格
以下命名風格通常區分彼此使用:
- b (單個小寫字母)
- B (單個大寫字母)
- lowercase(小寫)
- lower_case_with_underscores(帶下劃線的小寫)
- UPPERCASE(大寫)
- UPPER_CASE_WITH_UNDERSCORES(帶下劃線的大寫)
- CapitalizedWords(駝峰式,蒙古包式 whatever.)
Note: 使用駝峰式時,縮寫全部大寫,例如:HTTPServerError 好於 HttpServerError
- mixedCase (烏鬼頭)
- Capitalized_Words_With_Underscores (醜!不解釋!)
- _single_leading_underscore : 弱地 "內部使用" 指示器. 例如,from M import * 不會匯入下劃線開頭的物件
- single_trailing_underscore_ : 用來避免和 python 關鍵字衝突,例如:
Tkinter.Toplevel(master, class_='ClassName')
- __double_leading_underscore : 當對類屬性命名時,呼叫名改變 (在 FooBar 類內,__boo 變成了 _FooBar__boo;後面有介紹)
- __double_leading_and_trailing_underscore__ : "魔幻的" 物件或屬性,只生存於使用者控制的名稱空間。例如, __init__ ,__import__ 或 __file__ 。千萬不要臆造這種命名; only use them as documented.
6.2 規定性: 命名習慣
6.2.1 避免的命名
一定不要使用 l(唉歐兒) O(偶) I(艾) 作為單字元變數命名,在某些字型中,這些字母和數字1,0無法區分。
6.2.2包和模組名
模組應該使用簡短並且全小寫的命名,下劃線也可以使用以提升可讀性。
Python 包也應該使用簡短的全小寫名稱,儘管不鼓勵使用下劃線。
當 C/C++ 編寫的擴充套件模組伴隨一個提供更高級別介面的 python 模組時,C/C++ 模組命名應該以下劃線開頭(例如,_socket)。
6.2.3 類名
類名通常使用駝峰式命名習慣。
在類的介面有文件說明,並且主要用於 callable 的情況下,類都是 callable 的,call 一個類將返回一個新的類例項,例如 instance = Class()。如果類實現了 __call__() 函式,那麼類例項也將是 callable 的,類的命名也可以使用函式命名習慣。對於 builtin 函式的命名習慣,可以通過 dir(__builtins__) 檢視系統函式命名樣例。注意區分普通命名,異常名命名和 builtin 常量。
6.2.4 型別變數名
相對於短名稱如:T,AnyStr,Num,型別變數使用駝峰式命名習慣較好。另外建議在變數名前新增 _co 或 _contra 字首響應的宣告 covariant 或 contravariant 行為。例如:
from typing import TypeVar
VT_co = TypeVar('VT_co', covariant=True)KT_contra = TypeVar('KT_contra', contravariant=True)
6.2.5 異常名
異常應該是類,所以可以使用類命名習慣,但是,如果異常是個錯誤類,一般加上 "Error" 字尾。
6.2.6 全域性變數名
我們假設這些全域性變數只在一個模組內使用,這樣的話和函式的命名習慣是一樣的。設計為通過 from M import * 匯入的類應該使用 __all__ 機制避免匯出全域性變數,或者可以使用老式的習慣,給這些全域性變數名加上下劃線作為字首(表示這是非公有變數)。
6.2.7 函式名
例如,func or func_write_to_file 為了向後相容性,也可以使用 mixedCase 式命名風格。
6.2.8 函式和方法引數
例項方法第一個入參一定要是 self。
類方法第一個入參一定要是 cls。
如果函式入參名和保留關鍵字衝突,則字尾下劃線好過縮寫或者糟糕的拼寫。例如,class_ 好過 clss。
6.2.9 方法名和例項變數
使用函式命名風格即可。如果希望是私有方法或例項變數,則字首下劃線。為避免和子類的命名衝突,請使用雙下劃線字首命名。如果類 Foo 有一個屬性變數 __a,那麼通過 Foo.__a 是不能被訪問的。當然,固執的使用者仍然可以通過 Foo._Foo__a 訪問),一般來說,雙下劃線字首只是在避免子類屬性命名衝突的場景下使用。
6.2.10 常量
常量一般定義在模組級別。命名風格如:MAX_OVERFLOW 或 TOTAL 。
6.2.11 繼承設計
經常去思考類方法和例項變數(屬性)應該是公有的還是非公有的(嚴格意義上,python 沒有私有變數)。如果不確定,那就設定成非公有的。另一類屬性類別是子類 API 的一部分,(在其他語言中稱"protected")。有些類天生就是被設計為用來繼承的,當設計這種類時,注意哪些屬性是公有的,哪些是子類 API 的一部分,哪些是隻在基類中使用的。
來自仁慈的python之父的指導:
- 公有例項變數不應該有字首下劃線。
- 公有例項變數和保留關鍵字衝突時,變數名加字首下劃線避免,這比使用縮寫和其他糟糕的拼寫要好(除了 'cls',當一個變數或入參確定是一個類,特別是作為類方法的第一個入參時,'cls' 更惹人喜愛)。
- 對於簡單的公有資料屬性,不要使用複雜的存取函式,直接暴露屬性名。
- 如果設計繼承基類時,不希望子類訪問的屬性加雙下劃線字首。
6.2.12 公共和內部介面
文件說明的介面一般認為是公共介面,除非文件明確宣告為臨時或內部介面(為了相容性等其他原因),所有非文件說明的介面一般為內部介面。模組應該使用 __all__ 屬性明確宣告公共 API 名,如果 __all__ 為空,則表明模組沒有公共 API。儘管使用了 __all__ 屬性,內部介面(packages, modules, classes, functions, attributes or other names)仍然需要使用字首下劃線。如果包含的任何一個名稱空間(package, module or class)是內部的,那麼這個介面也被認為是內部介面。匯入名應該總是被視為實現細節。其他匯入模組一定不能依賴對此匯入名的間接訪問,除非它們是包含模組 API 的顯式文件說明的部分,例如os.path 或者一個 package 向子模組暴露函式的 __init__ 模組。
7 編碼建議
- 程式碼不應該以一種不利於其他 python 實現(PyPy, Jython, IronPython, Cython, Psyco 諸如此類)的方式編寫。 例如:不要使用 a += b 或 a = a + b 來實現就地字串連線,在庫的效能敏感部分,應該使用 ''.join() 的形式,這就能保證在不同的 python 實現中,連線動作可以線上性時間內完成。
- 和例如 None 這類 singleton 的比較,應該使用 is 或 is not 而不是 ==。另外,小心使用 if x 如果你的本意是 if x is not None,如果 x 是個布林變數值 false,那可就完蛋了。
- 儘管功能相同,從可讀性上考慮:
是:
if foo is not None:
否:
if not foo is None:
- 當使用 rich comparisons 實現排序操作時,最好是實現所有六種操作(__eq__,__ne__, __lt__, __le__, __gt__, __ge__)而不要依賴其他的程式碼去單獨實現某一類比較。為了減少勞動,functools.total_ordering() decorator 提供了一個生成缺失比較方法的工具。
- 使用 def 語句而不要使用賦值語句去直接繫結一個 lambda 表示式到識別符號上:
是:
def f(x): return 2*x
否:
f = lambda x: 2*x
- 賦值語句的使用消除了 lambda 表示式相對於顯式 def 語句的唯一好處,那就是它能夠嵌入到一個更大的表示式裡面。
- 捕獲的異常要說明 "錯誤出在哪裡了 ?" 而不是僅僅說明 "哎呀!出問題了!"。
- 正確使用異常連結。在 Python 3 中,應該使用 "raise X from Y" 來表示顯式替換並且不會丟失原始追溯。
- 當有意替換一個內部異常(Python 2: "raise X", Python 3.3+: raise X from Non)時,請確保將相關的詳細資訊轉移到新的異常(例如,將 KeyError 轉換為 AttributeError 時保留屬性名稱,或將原始異常的文字嵌入到新的異常訊息中)。
- 當在 Python 2 中丟擲異常時,使用 raise ValueError('message') 而不是老式的 raise ValueError, 'message',後者已經在 Python 3 中廢棄。由於使用了括號,可以避免行連續符的使用。
- 當捕獲異常時,儘可能提及具體的異常而不是使用一個赤裸裸的 except 子句。一個裸露的 except: 子句將捕獲 SystemExit 和 KeyboardInterrupt 異常,這樣的話就難於使用 control-c 中斷程式,並可能掩蓋其他問題。如果想要捕獲標誌程式錯誤的所有異常的話,用 except Exception:(裸露的 except 子句等同於 except BaseException:):
try:
import platform_specific_moduleexcept ImportError:
platform_specific_module = None
一個很好的經驗法則是將裸露的 except 子句僅用於以下兩種情況:
1、If the exception handler will be printing out or logging the traceback; at least the user will be aware that an error has occurred.
2、If the code needs to do some cleanup work, but then lets the exception propagate upwards with raise . try...finally can be a better way to handle this case.
- 當對捕獲的異常重新命名時,使用 2.6 版本引入的語法:
try:
process_data()except Exception as exc:
raise DataProcessingFailedError(str(exc))
- 當捕獲作業系統錯誤時,相對於內建的 errno 值,最好是使用 Python 3.3 中介紹的顯式異常層次結構。
- 對於所有的 try/except 子句,將 try 子句限制為必需的絕對最小程式碼量避免隱藏 bug:
是:
try:
value = collection[key]
except KeyError:
return key_not_found(key)
else:
return handle_value(value)
否:
try:
# Too broad!
return handle_value(collection[key])
except KeyError:
# Will also catch KeyError raised by handle_value()
return key_not_found(key)
- 特定程式碼塊的本地資源使用 with 語句確保使用後立即釋放,不能自動釋放的使用 try/finally 也可以。
- 除了申請和釋放資源,任何時候都應該使用單獨的函式和方法呼叫 Context managers,例如:
是:
with conn.begin_transaction():
do_stuff_in_transaction(conn)
否:
with conn:
do_stuff_in_transaction(conn)
- 函式返回語句要一致。在一個函式內的所有返回語句要麼都返回一個表示式,要麼都不返回。如果任何一個返回語句返回了表示式,那麼其他任何沒有返回值的語句應該明確宣告為 return None。在函式結束部分必須出現返回語句:
是:
def foo(x):
if x >= 0:
return math.sqrt(x)
else:
return None
def bar(x):
if x < 0:
return None
return math.sqrt(x)
否:
def foo(x):
if x >= 0:
return math.sqrt(x)
def bar(x):
if x < 0:
return
return math.sqrt(x)
- 相對於 string 模組,使用 string 方法要快的多並且與 unicode strings 共享相同的 API。當然了,除了需要考慮 2.0 版本之前 python 程式碼向後相容性的情況。
- 使用 ''.startswith() 和 ''.endswith() 而不是字串切片來檢查字首或字尾,例如:
是: if foo.startswith('bar'):
否: if foo[:3] == 'bar':
- 物件型別比較應該使用isinstance() 而不是直接比較:
是: if isinstance(obj, int):
否: if type(obj) is type(1):
- 當檢查一個物件是否為字串時,一定要注意這個物件也可能是 unicode 字串!在Python 2 中,string 和 unicode 擁有一個公共基類 basestring,因此可以這麼的:
if isinstance(obj, basestring):
在 Python 3 中,unicode 和 basestring 已然不復存在(there's only str),並且 bytes object 也不再視為一種 string 了,而是一個整形序列。
- 對於序列(字串,列表,元組)的判空操作:
是:
if not seq:
if seq:
否:
if len(seq):
if not len(seq):
- 不要使用尾隨空格。
- 不要使用 == 驗證布林值為 Ture 或 False:
是:
if greeting:
否:
if greeting == True:
也非:
if greeting is True:
8 結束語
本文主要參考的是PEP8 Python編碼規範和來源於網際網路上圈子內的的優秀實踐,吸納形成為我司的python編碼規範文件,計劃將這套規範用於指導我司python程式設計者進行作業,希望大家在日常的編碼中都能遵守,拿它當作鏡子,時常照照,對提升自身技能也大有裨益。當然,對本規範有任何不盡人意的地方都可以聯絡我進行修正完善。