【翻譯】Python PEP8編碼規範(中文版)

PortosHan發表於2021-02-25

原文連結:http://legacy.python.org/dev/peps/pep-0008/

itemdetail
PEP8
TitleStyle Guide for Python Code
Versionc451868df657
Last-Modified2016-06-08 10:43:53 -0400 (Wed, 08 Jun 2016)
AuthorGuido van Rossum <guido at python.org>, Barry Warsaw <barry at python.org>, Nick Coghlan <ncoghlan at gmail.com>
StatusActive
TypeProcess
Content-Typetext/x-rst
Created05-Jul-2001
Post-History05-Jul-2001, 01-Aug-2013


Introduction 介紹

本文提供的Python程式碼編碼規範基於Python主要發行版本的標準庫。Python的C語言實現的C程式碼規範請檢視相應的PEP指南1

這篇文件以及PEP 257(文件字串的規範)改編自Guido原始的《Python Style Guide》一文,同時新增了一些來自Barry的風格指南2

這篇規範指南隨著時間的推移而逐漸演變,隨著語言本身的變化,過去的約定也被淘汰了。

許多專案有自己的編碼規範,在出現規範衝突時,專案自身的規範優先。

A Foolish Consistency is the Hobgoblin of Little Minds 盡信書,則不如無書

Guido的一條重要的見解是程式碼閱讀比寫更加頻繁。這裡提供的指導原則主要用於提升程式碼的可讀性,使得在大量的Python程式碼中保持一致。就像PEP 20提到的,“Readability counts”。

這是一份關於一致性的風格指南。這份風格指南的風格一致性是非常重要的。更重要的是專案的風格一致性。在一個模組或函式的風格一致性是最重要的。

然而,應該知道什麼時候應該不一致,有時候編碼規範的建議並不適用。當存在模稜兩可的情況時,使用自己的判斷。看看其他的示例再決定哪一種是最好的,不要羞於發問。

特別是不要為了遵守PEP約定而破壞相容性!

幾個很好的理由去忽略特定的規則:

  1. 當遵循這份指南之後程式碼的可讀性變差,甚至是遵循PEP規範的人也覺得可讀性差。
  2. 與周圍的程式碼保持一致(也可能出於歷史原因),儘管這也是清理他人混亂(真正的Xtreme Programming風格)的一個機會。
  3. 有問題的程式碼出現在發現編碼規範之前,而且也沒有充足的理由去修改他們。
  4. 當程式碼需要相容不支援編碼規範建議的老版本Python。

Code lay-out 程式碼佈局

Indentation 縮排

每一級縮排使用4個空格。

續行應該與其包裹元素對齊,要麼使用圓括號、方括號和花括號內的隱式行連線來垂直對齊,要麼使用掛行縮排對齊3。當使用掛行縮排時,應該考慮到第一行不應該有引數,以及使用縮排以區分自己是續行。

推薦:

# 與左括號對齊
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)

四空格的規則對於續行是可選的。

可選:

# 掛行縮排不一定要用4個空格
foo = long_function_name(
  var_one, var_two,
  var_three, var_four)

當if語句的條件部分長到需要換行寫的時候,注意可以在兩個字元關鍵字的連線處(比如if),增加一個空格,再增加一個左括號來創造一個4空格縮排的多行條件。這會與if語句內同樣使用4空格縮排的程式碼產生視覺衝突。PEP沒有明確指明要如何區分i發的條件程式碼和內嵌程式碼。可使用的選項包括但不限於下面幾種情況:

# 沒有額外的縮排
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()

(可以參考下面關於是否在二進位制運算子之前或之後截斷的討論)
在多行結構中的大括號/中括號/小括號的右括號可以與內容對齊單獨起一行作為最後一行的第一個字元,就像這樣:

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',
)

Tabs or Spaces? 製表符還是空格?

空格是首選的縮排方式。
製表符只能用於與同樣使用製表符縮排的程式碼保持一致。
Python3不允許同時使用空格和製表符的縮排。
混合使用製表符和空格縮排的Python2程式碼應該統一轉成空格。
當在命令列加入-t選項執行Python2時,它會發出關於非法混用製表符與空格的警告。當使用–tt時,這些警告會變成錯誤。強烈建議使用這樣的引數。

Maximum Line Length 行的最大長度

所有行限制的最大字元數為79。
沒有結構化限制的大塊文字(文件字元或者註釋),每行的最大字元數限制在72。
限制編輯器視窗寬度可以使多個檔案並行開啟,並且在使用程式碼檢查工具(在相鄰列中顯示這兩個版本)時工作得很好。
大多數工具中的預設封裝破壞了程式碼的視覺化結構,使程式碼更難以理解。避免使用編輯器中預設配置的80視窗寬度,即使工具在幫你折行時在最後一列放了一個標記符。某些基於Web的工具可能根本不提供動態折行。
一些團隊更喜歡較長的行寬。如果程式碼主要由一個團隊維護,那這個問題就能達成一致,可以把行長度從80增加到100個字元(更有效的做法是將行最大長度增加到99個字元),前提是註釋和文件字串依然已72字元折行。
Python標準庫比較保守,需要將行寬限制在79個字元(文件/註釋限制在72)。
較長的程式碼行選擇Python在小括號,中括號以及大括號中的隱式續行方式。通過小括號內表示式的換行方式將長串折成多行。這種方式應該優先使用,而不是使用反斜槓續行。
反斜槓有時依然很有用。比如,比較長的,多個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())

(請參閱前面關於多行if-語句的討論,以獲得關於這種多行with-語句縮排的進一步想法。)
另一種類似情況是使用assert語句。
確保在續行進行適當的縮排。

Should a line break before or after a binary operator? 在二元運算子之前應該換行嗎?

幾十年來,推薦的風格是在二元運算子之後中斷。但是這回影響可讀性,原因有二:操作符一般分佈在螢幕上不同的列中,而且每個運算子被移到了運算元的上一行。下面例子這個情況就需要額外注意,那些變數是相加的,那些變數是相減的:

# 不推薦: 操作符離運算元太遠
income = (gross_wages +
          taxable_interest +
          (dividends - qualified_dividends) -
          ira_deduction -
          student_loan_interest)

為了解決這種可讀性的問題,數學家和他們的出版商遵循了相反的約定。Donald Knuth在他的Computers and Typesetting系列中解釋了傳統規則:“儘管段落中的公式總是在二元運算子和關係之後中斷,顯示出來的公式總是要在二元運算子之前中斷”4
遵循數學的傳統能產出更多可讀性高的程式碼:

# 推薦:運算子和運算元很容易進行匹配
income = (gross_wages
          + taxable_interest
          + (dividends - qualified_dividends)
          - ira_deduction
          - student_loan_interest)

在Python程式碼中,允許在二元運算子之前或之後中斷,只要本地的約定是一致的。對於新程式碼,建議使用Knuth的樣式。

Blank Lines 空行

頂層函式和類的定義,前後用兩個空行隔開。
類裡的方法定義用一個空行隔開。
相關的功能組可以用額外的空行(謹慎使用)隔開。一堆相關的單行程式碼之間的空白行可以省略(例如,一組虛擬實現 dummy implementations)。
在函式中使用空行來區分邏輯段(謹慎使用)。
Python接受control-L(即^L)換頁符作為空格;許多工具把這些字元當作頁面分隔符,所以你可以在檔案中使用它們來分隔相關段落。請注意,一些編輯器和基於Web的程式碼閱讀器可能無法識別control-L為換頁,將在其位置顯示另一個字形。

Source File Encoding 原始檔編碼

Python核心釋出版本中的程式碼總是以UTF-8格式編碼(或者在Python2中用ASCII編碼)。
使用ASCII(在Python2中)或UTF-8(在Python3中)編碼的檔案不應具有編碼宣告。
在標準庫中,非預設的編碼應該只用於測試,或者當一個註釋或者文件字串需要提及一個包含內ASCII字元編碼的作者名字的時候;否則,使用\x,\u,\U , 或者 \N 進行轉義來包含非ASCII字元。
對於Python 3和更高版本,標準庫規定了以下策略(參見 PEP 3131):Python標準庫中的所有識別符號必須使用ASCII識別符號,並在可行的情況下使用英語單詞(在許多情況下,縮寫和技術術語是非英語的)。此外,字串文字和註釋也必須是ASCII。唯一的例外是(a)測試非ASCII特徵的測試用例,以及(b)作者的名稱。作者的名字如果不使用拉丁字母拼寫,必須提供一個拉丁字母的音譯。
鼓勵具有全球受眾的開放原始碼專案採取類似的政策。

Imports 匯入

  • 匯入通常在分開的行,例如:
推薦: import os
     import sys

不推薦: import sys, os

但是可以這樣:

from subprocess import Popen, PIPE
  • 匯入總是位於檔案的頂部,在模組註釋和文件字串之後,在模組的全域性變數與常量之前。
    匯入應該按照以下順序分組:

    1. 標準庫匯入
    2. 相關第三方庫匯入
    3. 本地應用/庫特定匯入
      你應該在每一組匯入之間加入空行。
  • 推薦使用絕對路徑匯入,如果匯入系統沒有正確的配置(比如包裡的一個目錄在sys.path裡的路徑後),使用絕對路徑會更加可讀並且效能更好(至少能提供更好的錯誤資訊):

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 import *),因為這樣做會不知道名稱空間中存在哪些名字,會使得讀取介面和許多自動化工具之間產生混淆。對於萬用字元的匯入,有一個防禦性的做法,即將內部介面重新發布為公共API的一部分(例如,用可選加速器模組的定義覆蓋純Python實現的介面,以及重寫那些事先不知道的定義)。
    當以這種方式重新發布名稱時,以下關於公共和內部介面的準則仍然適用。

Module level dunder names 模組級的“呆”名

__all__ , __author__ , __version__ 等這樣的模組級“呆名“(也就是名字裡有兩個字首下劃線和兩個字尾下劃線),應該放在文件字串的後面,以及除from __future__ 之外的import表示式前面。Python要求將來在模組中的匯入,必須出現在除文件字串之外的其他程式碼之前。
比如:

"""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

String Quotes 字串引號

在Python中,單引號和雙引號字串是相同的。PEP不會為這個給出建議。選擇一條規則並堅持使用下去。當一個字串中包含單引號或者雙引號字元的時候,使用和最外層不同的符號來避免使用反斜槓,從而提高可讀性。
對於三引號字串,總是使用雙引號字元來與PEP 257中的文件字串約定保持一致。

Whitespace in Expressions and Statements 表示式和語句中的空格

Pet Peeves 不能忍受的事情

在下列情況下,避免使用無關的空格:

  • 緊跟在小括號,中括號或者大括號後。
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
  • 然而,冒號在切片中就像二元運算子,在兩邊應該有相同數量的空格(把它當做優先順序最低的操作符)。在擴充套件的切片操作中,所有的冒號必須有相同的間距。例外情況:當一個切片引數被省略時,空格就被省略了。
    推薦:
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]
  • 緊貼在函式引數的左括號之前。
Yes: spam(1)
No:  spam (1)
  • 緊貼索引或者切片的左括號之前。
Yes: dct['key'] = lst[index]
No:  dct ['key'] = lst [index]
  • 為了和另一個賦值語句對齊,在賦值運算子附件加多個空格。
    推薦:
x = 1
y = 2
long_variable = 3

不推薦:

x             = 1
y             = 2
long_variable = 3

Other Recommendations 其他建議

  • 避免在尾部新增空格。因為尾部的空格通常都看不見,會產生混亂:比如,一個反斜槓後面跟一個空格的換行符,不算續行標記。有些編輯器不會保留尾空格,並且很多專案(像CPython)在pre-commit的掛鉤呼叫中會過濾掉尾空格。
  • 總是在二元運算子兩邊加一個空格:賦值(=),增量賦值(+=,-=),比較(==,<,>,!=,<>,<=,>=,in,not,in,is,is not),布林(and, or, not)。
  • 如果使用具有不同優先順序的運算子,請考慮在具有最低優先順序的運算子周圍新增空格。有時需要通過自己來判斷;但是,不要使用一個以上的空格,並且在二元運算子的兩邊使用相同數量的空格。
    推薦:
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/for/while 放在同一行沒什麼問題,多行語句塊的情況不要這樣用,同樣也要避免程式碼行太長!
    最好別這樣:
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()

Comments 註釋

與程式碼相矛盾的註釋比沒有註釋還糟,當程式碼更改時,優先更新對應的註釋!
註釋應該是完整的句子。如果一個註釋是一個短語或句子,它的第一個單詞應該大寫,除非它是以小寫字母開頭的識別符號(永遠不要改變識別符號的大小寫!)。
如果註釋很短,結尾的句號可以省略。塊註釋一般由完整句子的一個或多個段落組成,並且每句話結束有個句號。
在句尾結束的時候應該使用兩個空格。
當用英文書寫時,遵循Strunk and White (譯註:《Strunk and White, The Elements of Style》)的書寫風格。
在非英語國家的Python程式設計師,請使用英文寫註釋,除非你120%的確信你的程式碼不會被使用其他語言的人閱讀。

Block Comments 塊註釋

塊註釋通常適用於跟隨它們的某些(或全部)程式碼,並縮排到與程式碼相同的級別。塊註釋的每一行開頭使用一個#和一個空格(除非塊註釋內部縮排文字)。
塊註釋內部的段落通過只有一個#的空行分隔。

Inline Comments 行內註釋

有節制地使用行內註釋。
行內註釋是與程式碼語句同行的註釋。行內註釋和程式碼至少要有兩個空格分隔。註釋由#和一個空格開始。
事實上,如果狀態明顯的話,行內註釋是不必要的,反而會分散注意力。比如說下面這樣就不需要:

x = x + 1                 # Increment x

但有時,這樣做很有用:

x = x + 1                 # Compensate for border

Documentation Strings 文件字串

編寫好的文件說明(也叫“docstrings”)的約定在PEP 257中永恆不變。

  • 要為所有的公共模組,函式,類以及方法編寫文件說明。非公共的方法沒有必要,但是應該有一個描述方法具體作用的註釋。這個註釋應該在def那一行之後。
  • PEP 257 描述了寫出好的文件說明相關的約定。特別需要注意的是,多行文件說明使用的結尾三引號應該自成一行,例如:
"""Return a foobang

Optional plotz says to frobnicate the bizbaz first.
"""
  • 對於單行的文件說明,尾部的三引號應該和文件在同一行。

Naming Conventions 命名規範

Python庫的命名規範很亂,從來沒能做到完全一致。但是目前有一些推薦的命名標準。新的模組和包(包括第三方框架)應該用這套標準,但當一個已有庫採用了不同的風格,推薦保持內部一致性。

Overriding Principle 最重要的原則

那些暴露給使用者的API介面的命名,應該遵循反映使用場景而不是實現的原則。

Descriptive: Naming Styles 描述:命名風格

有許多不同的命名風格。這裡能夠幫助大家識別正在使用什麼樣的命名風格,而不考慮他們為什麼使用。
以下是常見的命名方式:

  • b(單個小寫字母)
  • B(單個大寫字母)
  • lowercase 小寫字母
  • lower_case_with_underscores 使用下劃線分隔的小寫字母
  • UPPERCASE 大寫字母
  • UPPER_CASE_WITH_UNDERSCORES 使用下劃線分隔的大寫字母
  • CapitalizedWords(或者叫 CapWords,或者叫CamelCase 駝峰命名法 —— 這麼命名是因為字母看上去有起伏的外觀5)。有時候也被稱為StudlyCaps。
    注意:當在首字母大寫的風格中用到縮寫時,所有縮寫的字母用大寫,因此,HTTPServerError 比 HttpServerError 好。
  • mixedCase(不同於首字母大寫,第一個單詞的首字母小寫)
  • Capitalized_Words_With_Underscores(巨醜無比!)

也有用唯一的短字首把相關命名組織在一起的方法。這在Python中不常用,但還是提一下。比如,os.stat()函式中包含類似以st_mode,st_size,st_mtime這種傳統命名方式命名的變數。(這麼做是為了與 POSIX 系統的呼叫一致,以幫助程式設計師熟悉它。)
X11庫的所有公共函式都加了字首X。在Python裡面沒必要這麼做,因為屬性和方法在呼叫的時候都會用類名做字首,函式名用模組名做字首。
另外,下面這種用字首或結尾下劃線的特殊格式是被認可的(通常和一些約定相結合):

  • _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__:(雙下劃線開頭,雙下劃線結尾)“magic”物件或者存在於使用者控制的名稱空間內的屬性,例如:__init__,__import__或者__file__。除了作為文件之外,永遠不要命這樣的名。

Prescriptive: Naming Conventions 約定俗成:命名約定

Names to Avoid 應避免的名字

永遠不要使用字母‘l’(小寫的L),‘O’(大寫的O),或者‘I’(大寫的I)作為單字元變數名。
在有些字型裡,這些字元無法和數字0和1區分,如果想用‘l’,用‘L’代替。

Package and Module Names 包名和模組名

模組應該用簡短全小寫的名字,如果為了提升可讀性,下劃線也是可以用的。Python包名也應該使用簡短全小寫的名字,但不建議用下劃線。
當使用C或者C++編寫了一個依賴於提供高階(更物件導向)介面的Python模組的擴充套件模組,這個C/C++模組需要一個下劃線字首(例如:_socket)

Class Names 類名

類名一般使用首字母大寫的約定。
在介面被文件化並且主要被用於呼叫的情況下,可以使用函式的命名風格代替。
注意,對於內建的變數命名有一個單獨的約定:大部分內建變數是單個單詞(或者兩個單詞連線在一起),首字母大寫的命名法只用於異常名或者內部的常量。

Exception Names 異常名

因為異常一般都是類,所有類的命名方法在這裡也適用。然而,你需要在異常名後面加上“Error”字尾(如果異常確實是一個錯誤)。

Global Variable Names 全域性變數名

(我們希望這一類變數只在模組內部使用。)約定和函式命名規則一樣。
通過 from M import * 匯入的模組應該使用all機制去防止內部的介面對外暴露,或者使用在全域性變數前加下劃線的方式(表明這些全域性變數是模組內非公有)。

Function Names 函式名

函式名應該小寫,如果想提高可讀性可以用下劃線分隔。
大小寫混合僅在為了相容原來主要以大小寫混合風格的情況下使用(比如 threading.py),保持向後相容性。

Function and method arguments 函式和方法引數

始終要將 self 作為例項方法的的第一個引數。
始終要將 cls 作為類靜態方法的第一個引數。
如果函式的引數名和已有的關鍵詞衝突,在最後加單一下劃線比縮寫或隨意拼寫更好。因此 class_ 比 clss 更好。(也許最好用同義詞來避免這種衝突)

Method Names and Instance Variables 方法名和例項變數

遵循這樣的函式命名規則:使用下劃線分隔小寫單詞以提高可讀性。
在非共有方法和例項變數前使用單下劃線。
通過雙下劃線字首觸發Python的命名轉換規則來避免和子類的命名衝突。
Python通過類名對這些命名進行轉換:如果類 Foo 有一個叫 __a 的成員變數, 它無法通過 Foo.__a 訪問。(執著的使用者可以通過 Foo._Foo__a 訪問。)一般來說,字首雙下劃線用來避免類中的屬性命名與子類衝突的情況。
注意:關於__names的用法存在爭論(見下文)。

Constants 常量

常量通常定義在模組級,通過下劃線分隔的全大寫字母命名。例如: MAX_OVERFLOW 和 TOTAL。

Designing for inheritance 繼承的設計

始終要考慮到一個類的方法和例項變數(統稱:屬性)應該是共有還是非共有。如果存在疑問,那就選非共有;因為將一個非共有變數轉為共有比反過來更容易。
公共屬性是那些與類無關的客戶使用的屬性,並承諾避免向後不相容的更改。非共有屬性是那些不打算讓第三方使用的屬性;你不需要承諾非共有屬性不會被修改或被刪除。
我們不使用“私有(private)”這個說法,是因為在Python中目前還沒有真正的私有屬性(為了避免大量不必要的常規工作)。
另一種屬性作為子類API的一部分(在其他語言中通常被稱為“protected”)。有些類是專為繼承設計的,用來擴充套件或者修改類的一部分行為。當設計這樣的類時,要謹慎決定哪些屬性時公開的,哪些是作為子類的API,哪些只能在基類中使用。
貫徹這樣的思想,一下是一些讓程式碼Pythonic的準則:

  • 公共屬性不應該有字首下劃線。
  • 如果公共屬性名和關鍵字衝突,在屬性名之後增加一個下劃線。這比縮寫和隨意拼寫好很多。(然而,儘管有這樣的規則,在作為引數或者變數時,‘cls’是表示‘類’最好的選擇,特別是作為類方法的第一個引數。)
    注意1:參考之前的類方法引數命名建議
  • 對於單一的共有屬性資料,最好直接對外暴露它的變數名,而不是通過負責的 存取器(accessor)/突變(mutator) 方法。請記住,如果你發現一個簡單的屬性需要成長為一個功能行為,那麼Python為這種將來會出現的擴充套件提供了一個簡單的途徑。在這種情況下,使用屬性去隱藏屬性資料訪問背後的邏輯。
    注意1:屬性只在new-style類中起作用。
    注意2:儘管功能方法對於類似快取的負面影響比較小,但還是要儘量避免。
    注意3:屬性標記會讓呼叫者認為開銷(相當的)小,避免用屬性做開銷大的計算。
  • 如果你的類打算用來繼承的話,並且這個類裡有不希望子類使用的屬性,就要考慮使用雙下劃線字首並且沒有字尾下劃線的命名方式。這會呼叫Python的命名轉換演算法,將類的名字加入到屬性名裡。這樣做可以幫助避免在子類中不小心包含了相同的屬性名而產生的衝突。
    注意1:只有類名才會整合進屬性名,如果子類的屬性名和類名和父類都相同,那麼你還是會有命名衝突的問題。
    注意2:命名轉換會在某些場景使用起來不太方便,例如除錯,__getattr__()。然而命名轉換的演算法有很好的文件說明並且很好操作。
    注意3:不是所有人都喜歡命名轉換。儘量避免意外的名字衝突和潛在的高階呼叫。

Public and internal interfaces 公共和內部的介面

任何向後相容保證只適用於公共介面,因此,使用者清晰地區分公共介面和內部介面非常重要。
文件化的介面被認為是公開的,除非文件明確宣告它們是臨時或內部介面,不受通常的向後相容性保證。所有未記錄的介面都應該是內部的。
為了更好地支援內省(introspection),模組應該使用__all__屬性顯式地在它們的公共API中宣告名稱。將__all__設定為空列表表示模組沒有公共API。
即使通過__all__設定過,內部介面(包,模組,類,方法,屬性或其他名字)依然需要單個下劃線字首。
如果一個名稱空間(包,模組,類)被認為是內部的,那麼包含它的介面也應該被認為是內部的。
匯入的名稱應該始終被視作是一個實現的細節。其他模組必須不能間接訪問這樣的名稱,除非它是包含它的模組中有明確的文件說明的API,例如 os.path 或者是一個包裡從子模組公開函式介面的 __init__ 模組。

Programming Recommendations 程式設計建議

  • 程式碼應該用不損害其他Python實現的方式去編寫(PyPy,Jython,IronPython,Cython,Psyco 等)。
    比如,不要依賴於在CPython中高效的內建字元連線語句 a += b 或者 a = a + b。這種優化甚至在CPython中都是脆弱的(它只適用於某些型別)並且沒有出現在不使用引用計數的實現中。在效能要求比較高的庫中,可以種 ”.join() 代替。這可以確保字元關聯在不同的實現中都可以以線性時間發生。
  • 和像None這樣的單例物件進行比較的時候應該始終用 is 或者 is not,永遠不要用等號運算子。
    另外,如果你在寫 if x 的時候,請注意你是否表達的意思是 if x is not None。舉個例子,當測試一個預設值為None的變數或者引數是否被設定為其他值的時候。這個其他值應該是在上下文中能成為bool型別false的值。
  • 使用 is not 運算子,而不是 not … is 。雖然這兩種表示式在功能上完全相同,但前者更易於閱讀,所以優先考慮。
    推薦:
if foo is not None:

不推薦:

if not foo is None:
  • 當使用富比較(rich comparisons,一種複雜的物件間比較的新機制,允許返回值不為-1,0,1)實現排序操作的時候,最好實現全部的六個操作符(__eq__, __ne__, __lt__, __gt__, __ge__)而不是依靠其他的程式碼去實現特定的比較。
    為了最大程度減少這一過程的開銷, functools.total_ordering() 修飾符提供了用於生成缺少的比較方法的工具。
    PEP 207 指出Python實現了反射機制。因此,解析器會將 y > x 轉變為 x < y,將 y >= x 轉變為 x <= y,也會轉換x == y 和 x != y的引數。sort() 和 min()方法確保使用<操作符,max()使用>操作符。然而,最好還是實現全部六個操作符,以免在其他地方出現衝突。
  • 始終使用def表示式,而不是通過賦值語句將lambda表示式繫結到一個變數上。
    推薦:
def f(x): return 2*x

不推薦:

f = lambda x: 2*x

第一個形式意味著生成的函式物件的名稱是“f”而不是泛型“< lambda >”。這在回溯和字串顯示的時候更有用。賦值語句的使用消除了lambda表示式優於顯式def表示式的唯一優勢(即lambda表示式可以內嵌到更大的表示式中)。

  • 從Exception繼承異常,而不是BaseException。直接繼承BaseException的異常適用於幾乎不用來捕捉的異常。
    設計異常的等級,要基於撲捉異常程式碼的需要,而不是異常丟擲的位置。以程式設計的方式去回答“出了什麼問題?”,而不是隻是確認“出現了問題”(內建異常結構的例子參考 PEP 3151
    類的命名規範適用於這裡,但是你需要新增一個“Error”的字尾到你的異常類,如果異常是一個Error的話。非本地流控制或者其他形式的訊號的非錯誤異常不需要特殊的字尾。

  • 適當地使用異常連結。在Python 3裡,為了不丟失原始的根源,可以顯式指定“raise X from Y”作為替代。
    當故意替換一個內部異常時(Python 2 使用“raise X”, Python 3.3 之後 使用 “raise X from None”),確保相關的細節轉移到新的異常中(比如把AttributeError轉為KeyError的時候保留屬性名,或者將原始異常資訊的文字內容內嵌到新的異常中)。

  • 在Python 2中丟擲異常時,使用 rasie ValueError(‘message’) 而不是用老的形式 raise ValueError, ‘message’。
    第二種形式在Python3 的語法中不合法
    使用小括號,意味著當異常裡的引數非常長,或者包含字串格式化的時候,不需要使用換行符。

  • 當捕獲到異常時,如果可以的話寫上具體的異常名,而不是隻用一個except: 塊。
    比如說:

try:
    import platform_specific_module
except ImportError:
    platform_specific_module = None

如果只有一個except: 塊將會捕獲到SystemExit和KeyboardInterrupt異常,這樣會很難通過Control-C中斷程式,而且會掩蓋掉其他問題。如果你想捕獲所有指示程式出錯的異常,使用 except Exception: (只有except等價於 except BaseException:)。
兩種情況不應該只使用‘excpet’塊:

  1. 如果異常處理的程式碼會列印或者記錄log;至少讓使用者知道發生了一個錯誤。
  2. 如果程式碼需要做清理工作,使用 raise..try…finally 能很好處理這種情況並且能讓異常繼續上浮。

    • 當給捕捉的異常繫結一個名字時,推薦使用在Python 2.6中加入的顯式命名繫結語法:
try:
    process_data()
except Exception as exc:
    raise DataProcessingFailedError(str(exc))

為了避免和原來基於逗號分隔的語法出現歧義,Python3只支援這一種語法。

  • 當捕捉作業系統的錯誤時,推薦使用Python 3.3 中errno內定數值指定的異常等級。

  • 另外,對於所有的 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也可以。
  • 無論何時獲取和釋放資源,都應該通過單獨的函式或方法呼叫上下文管理器。舉個例子:
    推薦:
with conn.begin_transaction():
    do_stuff_in_transaction(conn)

不推薦:

with conn:
    do_stuff_in_transaction(conn)

第二個例子沒有提供任何資訊去指明__enter____exit__方法在事務之後做出了關閉連線之外的其他事情。這種情況下,明確指明非常重要。

  • 返回的語句保持一致。函式中的返回語句都應該返回一個表示式,或者都不返回。如果一個返回語句需要返回一個表示式,那麼在沒有值可以返回的情況下,需要用 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)
  • 使用字串方法代替字串模組。
    字串方法總是更快,並且和unicode字串分享相同的API。如果需要相容Python2.0之前的版本可以不用考慮這個規則。
  • 使用 ”.startswith() 和 ”.endswith() 代替通過字串切割的方法去檢查字首和字尾。
    startswith()和endswith()更乾淨,出錯機率更小。比如:
推薦: if foo.startswith('bar'):
糟糕: if foo[:3] == 'bar':
  • 物件型別的比較應該用isinstance()而不是直接比較type。
正確: if isinstance(obj, int):
糟糕: if type(obj) is type(1):

當檢查一個物件是否為string型別時,記住,它也有可能是unicode string!在Python2中,str和unicode都有相同的基類:basestring,所以你可以這樣:

if isinstance(obj, basestring):

注意,在Python3中,unicode和basestring都不存在了(只有str)並且bytes型別的物件不再是string型別的一種(它是整數序列)

  • 對於序列來說(strings,lists,tuples),可以使用空序列為false的情況。
正確: if not seq:
      if seq:
糟糕: if len(seq):
      if not len(seq):
  • 書寫字串時不要依賴單詞結尾的空格,這樣的空格在視覺上難以區分,有些編輯器會自動去掉他們(比如 reindent.py (譯註:re indent 重新縮排))
  • 不要用 == 去和True或者False比較:
正確: if greeting:
糟糕: if greeting == True:
更糟: if greeting is True:

Function Annotations 功能註釋

隨著PEP 484的引入,功能型註釋的風格規範有些變化。

  • 為了向前相容,在Python3程式碼中的功能註釋應該使用 PEP 484的語法規則。(在前面的章節中對註釋有格式化的建議。)
  • 不再鼓勵使用之前在PEP中推薦的實驗性樣式。
  • 然而,在stdlib庫之外,在PEP 484中的實驗性規則是被鼓勵的。比如用PEP 484的樣式標記大型的第三方庫或者應用程式,回顧新增這些註釋是否簡單,並觀察是否增加了程式碼的可讀性。
  • Python的標準庫程式碼應該保守使用這種註釋,但新的程式碼或者大型的重構可以使用這種註釋。
  • 如果程式碼希望對功能註釋有不同的用途,建議在檔案的頂部增加一個這種形式的註釋:
# type: ignore

這會告訴檢查器忽略所有的註釋。(在 PEP 484中可以找到從型別檢查器禁用投訴的更細粒度的方法。)

  • 像linters一樣,型別檢測器是可選的可獨立的工具。預設情況下,Python直譯器不應該因為型別檢查而發出任何訊息,也不應該基於註釋改變它們的行為。

  • 不想使用型別檢測的使用者可以忽略他們。然而,第三方庫的使用者可能希望在這些庫上執行型別檢測。為此, PEP 484 建議使用存根檔案型別:.pyi檔案,這種檔案型別相比於.py檔案會被型別檢測器讀取。存根檔案可以和庫一起,或者通過typeshed repo6獨立釋出(通過庫作者的許可)

  • 對於需要向後相容的程式碼,可以以註釋的形式新增功能型註釋。參見PEP 484的相關部分7

參考


  1. PEP 7, Style Guide for C Code, van Rossum
  2. Barry’s GNU Mailman style guide http://barry.warsaw.us/software/STYLEGUIDE.txt
  3. 掛行縮排是一種型別設定樣式,其中除第一行之外,段落中的所有行都縮排。在Python中,這個術語是用來描述一種風格:在被括號括起來的語句中,左括號是這一行最後一個非空格字元,隨後括號內的內容每一行進行縮排,直到遇到右括號。
  4. Donald Knuth’s The TeXBook, pages 195 and 196
  5. http://www.wikipedia.com/wiki/CamelCase
  6. Typeshed repo https://github.com/python/typeshed
  7. Suggested syntax for Python 2.7 and straddling code https://www.python.org/dev/peps/pep-0484/#suggested-syntax-for-python-2-7-and-straddling-code

相關文章