PEP8 原文地址:http://legacy.python.org/dev/peps/pep-0008/
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約定而破壞兼容性!
幾個很好的理由去忽略特定的規則:
- 當遵循這份指南之後代碼的可讀性變差,甚至是遵循PEP規範的人也覺得可讀性差。
- 與周圍的代碼保持一致(也可能出於歷史原因),儘管這也是清理他人混亂(真正的Xtreme - Programming風格)的一個機會。
- 有問題的代碼出現在發現編碼規範之前,而且也沒有充足的理由去修改他們。
- 當代碼需要兼容不支持編碼規範建議的老版本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
導入總是位於文件的頂部,在模塊註釋和文檔字符串之後,在模塊的全局變量與常量之前。
導入應該按照以下順序分組:
- 標準庫導入
- 相關第三方庫導入
- 本地應用/庫特定導入
你應該在每一組導入之間加入空行。
推薦使用絕對路徑導入,如果導入系統沒有正確的配置(比如包裏的一個目錄在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不會爲這個給出建議。選擇一條規則並堅持使用下去。當一個字符串中包含單引號或者雙引號字符的時候,使用和最外層不同的符號來避免使用反斜槓,從而提高可讀性。
對於三引號字符串,總是使用雙引號字符來與PEP257參考實例 中的文檔字符串約定保持一致。
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’塊:-
如果異常處理的代碼會打印或者記錄log;至少讓用戶知道發生了一個錯誤。
-
如果代碼需要做清理工作,使用 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的相關部分。
參考實例
微信開發包,python實現, wechat_sdk開發,可做 規範參考和類的設計學習
from __future__ import unicode_literals
import time
from wechat_sdk.lib.crypto import BasicCrypto
from wechat_sdk.lib.request import WechatRequest
from wechat_sdk.exceptions import NeedParamError
from wechat_sdk.utils import disable_urllib3_warning
class WechatConf(object):
""" WechatConf 配置類
該類將會存儲所有和微信開發相關的配置信息, 同時也會維護配置信息的有效性.
"""
def __init__(self, **kwargs):
"""
:param kwargs: 配置信息字典, 可用字典 key 值及對應解釋如下:
'token': 微信 Token
'appid': App ID
'appsecret': App Secret
'encrypt_mode': 加解密模式 ('normal': 明文模式, 'compatible': 兼容模式, 'safe': 安全模式(默認))
'encoding_aes_key': EncodingAESKey 值 (傳入此值必須保證同時傳入 token, appid, 否則拋出異常)
'access_token_getfunc': access token 獲取函數 (用於單機及分佈式環境下, 具體格式參見文檔)
'access_token_setfunc': access token 寫入函數 (用於單機及分佈式環境下, 具體格式參見文檔)
'access_token_refreshfunc': access token 刷新函數 (用於單機及分佈式環境下, 具體格式參見文檔)
'access_token': 直接導入的 access token 值, 該值需要在上一次該類實例化之後手動進行緩存並在此處傳入, 如果不
傳入, 將會在需要時自動重新獲取 (傳入 access_token_getfunc 和 access_token_setfunc 函數
後將會自動忽略此處的傳入值)
'access_token_expires_at': 直接導入的 access token 的過期日期, 該值需要在上一次該類實例化之後手動進行緩存
並在此處傳入, 如果不傳入, 將會在需要時自動重新獲取 (傳入 access_token_getfunc
和 access_token_setfunc 函數後將會自動忽略此處的傳入值)
'jsapi_ticket_getfunc': jsapi ticket 獲取函數 (用於單機及分佈式環境下, 具體格式參見文檔)
'jsapi_ticket_setfunc': jsapi ticket 寫入函數 (用於單機及分佈式環境下, 具體格式參見文檔)
'jsapi_ticket_refreshfunc': jsapi ticket 刷新函數 (用於單機及分佈式環境下, 具體格式參見文檔)
'jsapi_ticket': 直接導入的 jsapi ticket 值, 該值需要在上一次該類實例化之後手動進行緩存並在此處傳入, 如果不
傳入, 將會在需要時自動重新獲取 (傳入 jsapi_ticket_getfunc 和 jsapi_ticket_setfunc 函數
後將會自動忽略此處的傳入值)
'jsapi_ticket_expires_at': 直接導入的 jsapi ticket 的過期日期, 該值需要在上一次該類實例化之後手動進行緩存
並在此處傳入, 如果不傳入, 將會在需要時自動重新獲取 (傳入 jsapi_ticket_getfunc
和 jsapi_ticket_setfunc 函數後將會自動忽略此處的傳入值)
'partnerid': 財付通商戶身份標識, 支付權限專用
'partnerkey': 財付通商戶權限密鑰 Key, 支付權限專用
'paysignkey': 商戶簽名密鑰 Key, 支付權限專用
'checkssl': 是否檢查 SSL, 默認不檢查 (False), 可避免 urllib3 的 InsecurePlatformWarning 警告
:return:
"""
self.__request = WechatRequest()
if kwargs.get('checkssl') is not True:
disable_urllib3_warning() # 可解決 InsecurePlatformWarning 警告
self.__token = kwargs.get('token')
self.__appid = kwargs.get('appid')
self.__appsecret = kwargs.get('appsecret')
self.__encrypt_mode = kwargs.get('encrypt_mode', 'safe')
self.__encoding_aes_key = kwargs.get('encoding_aes_key')
self.__crypto = None
self._update_crypto()
self.__access_token_getfunc = kwargs.get('access_token_getfunc')
self.__access_token_setfunc = kwargs.get('access_token_setfunc')
self.__access_token_refreshfunc = kwargs.get('access_token_refreshfunc')
self.__access_token = kwargs.get('access_token')
self.__access_token_expires_at = kwargs.get('access_token_expires_at')
self.__jsapi_ticket_getfunc = kwargs.get('jsapi_ticket_getfunc')
self.__jsapi_ticket_setfunc = kwargs.get('jsapi_ticket_setfunc')
self.__jsapi_ticket_refreshfunc = kwargs.get('jsapi_ticket_refreshfunc')
self.__jsapi_ticket = kwargs.get('jsapi_ticket')
self.__jsapi_ticket_expires_at = kwargs.get('jsapi_ticket_expires_at')
self.__partnerid = kwargs.get('partnerid')
self.__partnerkey = kwargs.get('partnerkey')
self.__paysignkey = kwargs.get('paysignkey')
@property
def token(self):
""" 獲取當前 Token """
self._check_token()
return self.__token
@token.setter
def token(self, token):
""" 設置當前 Token """
self.__token = token
self._update_crypto() # 改動 Token 需要重新更新 Crypto
@property
def appid(self):
""" 獲取當前 App ID """
return self.__appid
@property
def appsecret(self):
""" 獲取當前 App Secret """
return self.__appsecret
def set_appid_appsecret(self, appid, appsecret):
""" 設置當前 App ID 及 App Secret"""
self.__appid = appid
self.__appsecret = appsecret
self._update_crypto() # 改動 App ID 後需要重新更新 Crypto
@property
def encoding_aes_key(self):
""" 獲取當前 EncodingAESKey """
return self.__encoding_aes_key
@encoding_aes_key.setter
def encoding_aes_key(self, encoding_aes_key):
""" 設置當前 EncodingAESKey """
self.__encoding_aes_key = encoding_aes_key
self._update_crypto() # 改動 EncodingAESKey 需要重新更新 Crypto
@property
def encrypt_mode(self):
return self.__encrypt_mode
@encrypt_mode.setter
def encrypt_mode(self, encrypt_mode):
""" 設置當前加密模式 """
self.__encrypt_mode = encrypt_mode
self._update_crypto()
@property
def crypto(self):
""" 獲取當前 Crypto 實例 """
return self.__crypto
@property
def access_token(self):
""" 獲取當前 access token 值, 本方法會自行維護 access token 有效性 """
self._check_appid_appsecret()
if callable(self.__access_token_getfunc):
self.__access_token, self.__access_token_expires_at = self.__access_token_getfunc()
if self.__access_token:
now = time.time()
if self.__access_token_expires_at - now > 60:
return self.__access_token
self.grant_access_token() # 從騰訊服務器獲取 access token 並更新
return self.__access_token
@property
def jsapi_ticket(self):
""" 獲取當前 jsapi ticket 值, 本方法會自行維護 jsapi ticket 有效性 """
self._check_appid_appsecret()
if callable(self.__jsapi_ticket_getfunc):
self.__jsapi_ticket, self.__jsapi_ticket_expires_at = self.__jsapi_ticket_getfunc()
if self.__jsapi_ticket:
now = time.time()
if self.__jsapi_ticket_expires_at - now > 60:
return self.__jsapi_ticket
self.grant_jsapi_ticket() # 從騰訊服務器獲取 jsapi ticket 並更新
return self.__jsapi_ticket
@property
def partnerid(self):
""" 獲取當前財付通商戶身份標識 """
return self.__partnerid
@property
def partnerkey(self):
""" 獲取當前財付通商戶權限密鑰 Key """
return self.__partnerkey
@property
def paysignkey(self):
""" 獲取商戶簽名密鑰 Key """
return self.__paysignkey
def grant_access_token(self):
"""
獲取 access token 並更新當前配置
:return: 返回的 JSON 數據包 (傳入 access_token_refreshfunc 參數後返回 None)
"""
self._check_appid_appsecret()
if callable(self.__access_token_refreshfunc):
self.__access_token, self.__access_token_expires_at = self.__access_token_refreshfunc()
return
response_json = self.__request.get(
url="https://api.weixin.qq.com/cgi-bin/token",
params={
"grant_type": "client_credential",
"appid": self.__appid,
"secret": self.__appsecret,
},
access_token=self.__access_token
)
self.__access_token = response_json['access_token']
self.__access_token_expires_at = int(time.time()) + response_json['expires_in']
if callable(self.__access_token_setfunc):
self.__access_token_setfunc(self.__access_token, self.__access_token_expires_at)
return response_json
def grant_jsapi_ticket(self):
"""
獲取 jsapi ticket 並更新當前配置
:return: 返回的 JSON 數據包 (傳入 jsapi_ticket_refreshfunc 參數後返回 None)
"""
self._check_appid_appsecret()
if callable(self.__jsapi_ticket_refreshfunc):
self.__jsapi_ticket, self.__jsapi_ticket_expires_at = self.__jsapi_ticket_refreshfunc()
return
response_json = self.__request.get(
url="https://api.weixin.qq.com/cgi-bin/ticket/getticket",
params={
"type": "jsapi",
},
access_token=self.access_token,
)
self.__jsapi_ticket = response_json['ticket']
self.__jsapi_ticket_expires_at = int(time.time()) + response_json['expires_in']
if callable(self.__jsapi_ticket_setfunc):
self.__jsapi_ticket_setfunc(self.__jsapi_ticket, self.__jsapi_ticket_expires_at)
return response_json
def get_access_token(self):
"""
獲取 Access Token 及 Access Token 過期日期, 僅供緩存使用, 如果希望得到原生的 Access Token 請求數據請使用 :func:`grant_token`
**僅爲兼容 v0.6.0 以前版本使用, 自行維護 access_token 請使用 access_token_setfunc 和 access_token_getfunc 進行操作**
:return: dict 對象, key 包括 `access_token` 及 `access_token_expires_at`
"""
self._check_appid_appsecret()
return {
'access_token': self.access_token,
'access_token_expires_at': self.__access_token_expires_at,
}
def get_jsapi_ticket(self):
"""
獲取 Jsapi Ticket 及 Jsapi Ticket 過期日期, 僅供緩存使用, 如果希望得到原生的 Jsapi Ticket 請求數據請使用 :func:`grant_jsapi_ticket`
**僅爲兼容 v0.6.0 以前版本使用, 自行維護 jsapi_ticket 請使用 jsapi_ticket_setfunc 和 jsapi_ticket_getfunc 進行操作**
:return: dict 對象, key 包括 `jsapi_ticket` 及 `jsapi_ticket_expires_at`
"""
self._check_appid_appsecret()
return {
'jsapi_ticket': self.jsapi_ticket,
'jsapi_ticket_expires_at': self.__jsapi_ticket_expires_at,
}
def _check_token(self):
"""
檢查 Token 是否存在
:raises NeedParamError: Token 參數沒有在初始化的時候提供
"""
if not self.__token:
raise NeedParamError('Please provide Token parameter in the construction of class.')
def _check_appid_appsecret(self):
"""
檢查 AppID 和 AppSecret 是否存在
:raises NeedParamError: AppID 或 AppSecret 參數沒有在初始化的時候完整提供
"""
if not self.__appid or not self.__appsecret:
raise NeedParamError('Please provide app_id and app_secret parameters in the construction of class.')
def _update_crypto(self):
"""
根據當前配置內容更新 Crypto 類
"""
if self.__encrypt_mode in ['compatible', 'safe'] and self.__encoding_aes_key is not None:
if self.__token is None or self.__appid is None:
raise NeedParamError('Please provide token and appid parameters in the construction of class.')
self.__crypto = BasicCrypto(self.__token, self.__encoding_aes_key, self.__appid)
else:
self.__crypto = None