Python風格指南(Google版)

Python風格指南(Google版)

Python 是 Google主要的腳本語言。這本風格指南主要包含的是針對python的編程準則。使得Python代碼編寫更加規範、優美。

Python語言規範

Lint

對你的代碼允許pylint

定義:

pylint是一個在Python源代碼中查找bug的工具。對於C和C++這樣的不那麼動態的語言,這些bug通常由編譯器來捕獲。由於Python的動態特性,有些警告可能不對。不過僞告警應該很少。

優點:

可以捕獲容易忽視的錯誤,例如輸入錯誤、使用未賦值的變量等。

缺點:

pylint並不不完美。要利用其優勢,我們有時侯需要:

  • 圍繞着它來寫代碼
  • 抑制其告警
  • 改進它
  • 忽略它
結論:

確保對你的代碼運行pylint。抑制不準確的警告,以便能夠將其他警告暴露出來。

你可以通過設置一個行註釋來抑制告警。

dict = 'something awful'  # Bad Idea... pylint: disable=redefined-builtin 

pylint警告是以一個數字編號(如C0112)和一個符號名(如empty-docstring)來標識的。在編寫新代碼或更新已有代碼時對告警進行抑制,推薦使用符號名來標識。

如果警告的符號名不夠見名知意,那麼請對其增加一個詳細解釋。

採用這種抑制方式的好處是我們可以輕鬆查找抑制並回顧它們。

你可以使用命令pylint --list-msgs來獲取pylint告警列表. 你可以使用命令pylint --help-msg=C6409,以獲取關於特定消息的更多信息。

相比較於之前使用的pylint: disable-msg,本文推薦使用pylint: disable

要抑制”參數未使用”告警,你可以用””作爲參數標識符, 或者在參數名前加”unused”。遇到不能改變參數名的情況,你可以通過在函數開頭”提到”它們來消除告警。例如

def foo(a, unused_b, unused_c, d=None, e=None):
    _ = d, e
    return a

導入

僅對包和模塊使用導入

定義:

模塊間共享代碼的重用機制。

優點:

命名空間管理約定十分簡單。每個標識符的源都用一種一致的方式指示。x.Obj表示Obj對象定義在模塊X中。

缺點:

模塊名仍有可能衝突。有些模塊名太長,不太方便。

結論:
  1. 使用import x來導入包和模塊。
  2. 使用from x import y,其中x是包前綴,y是不帶前綴的模塊名。
  3. 使用from x import y as z,如果兩個要導入的模塊都叫做y或者有太長了。
    from sound.effects import echo
    ...
    echo.EchoFilter(input, output, delay=0.7, atten=4)
  4. 導入時不要使用相對名稱。即使模塊在同一個包中,也要使用完整包名。這能幫助你避免無意間導入一個包兩次。

使用模塊的全路徑名來導入每個模塊。

優點:

避免模塊名衝突。查找包更容易。

缺點:

部署代碼變難,因爲你必須複製包層次。

結論:

所用的新代碼都應該用完整包名來導入每個模塊。

# Reference in code with complete name. 
import sound.effects.echo
 
# Reference in code with just module name (preferred). 
from sound.effects import echo

異常

允許使用異常,但必須小心。

定義:

異常是一種跳出代碼塊的正常控制流程來處理錯誤或者其他異常條件的方式。

優點:

正常操作代碼的控制流不會和錯誤處理代碼混在一起。當某種條件發生時,它也允許控制流跳過多個框架。例如,一步跳出N個嵌套的函數,而不必繼續執行錯誤的代碼。

缺點:

可能會導致讓人困惑的控制流。調用庫時容易錯過錯誤情況。

結論:

異常必須遵守特定條件:

  1. 像這樣觸發異常:raise MyException("Error message")或者raise MyException。不要使用兩個參數的形式(raise MyException,"Error message")或者過時的字符串異常( raise "Error message")
  2. 模塊或包應該定義自己的特定域的異常基類,這個基類應該從內建的Exception類繼承。模塊的異常基類應該叫做”Error”。
  3. 永遠不要使用except:語句來捕獲所有異常,也不要捕獲Exception或者StandardError,除非你打算重新觸發該異常,或者你已經在當前線程的最外層(記得還是要打印一條錯誤消息)。在異常這方面,Python非常寬容,except:真的會捕獲包括Python語法錯誤在內的任何錯誤。使用 except:很容易隱藏真正的bug。
  4. 儘量減少try/except塊中的代碼量。try塊的體積越大,期望之外的異常就越容易被觸發。這種情況下,try/except塊將隱藏真正的錯誤。
  5. 使用finally子句來執行那些無論try塊中有沒有異常都應該被執行的代碼。這對於清理資源常常很有用,例如關閉文件。
  6. 當捕獲異常時, 使用as而不要用逗號。
    try:
     raise Error
    except Error as error:
     pass

全局變量

避免全局變量

定義:

定義模塊級的變量。

優點:

偶爾使用。

缺點:

導入時可能改變模塊行爲,因爲導入模塊時會對模塊級變量賦值。

結論:

避免使用全局變量,用類變量來代替。但有一些例外:

  1. 腳本的默認選項。
  2. 模塊級常量。
  3. 有時候用全局變量來緩存值或者作爲函數返回值很有用。
  4. 如果需要,全局變量應該僅在模塊內部作用,並通過模塊級的公共函數來訪問。

嵌套、局部、內部類或函數

鼓勵使用嵌套、本地、內部類或函數

定義:

類可以定在方法、函數或者類中。函數可以定義在方法或函數中,封閉區間中定義的變量對嵌套函數是隻讀的。

優點:

允許定義僅用於有效函數的工具類和函數。

缺點:

嵌套類或局部類的實例不能序列化(picked)。

結論:

推薦使用。

列表推導(List Comprehensions)

可以在簡單情況下使用。

定義:

列表推導(list comprehensions)與生成器表達式(generator expression)提供了一種簡潔高效的方式來創建列表和迭代器,而不必藉助map(),filter(),或者lambda。

優點:

簡單的列表推導可以比其它的列表創建方法更加清晰簡單。生成器表達式可以十分高效,因爲它們避免了創建整個列表。

缺點:

複雜的列表推導或者生成器表達式可能難以閱讀。

結論:

適用於簡單情況。每個部分應該單獨置於一行: 映射表達式,for語句,過濾器表達式。禁止多重for語句或過濾器表達式。複雜情況下還是使用循環。

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 for x in range(10)]
 
  eat(jelly_bean for jelly_bean in jelly_beans
      if jelly_bean.color == 'black')

默認迭代器和操作符

如果類型支持,就使用默認迭代器和操作符.比如列表、字典及文件等。

定義:

容器類型,像字典和列表,定義了默認的迭代器和關係測試操作符(in和not in)。

優點:

默認操作符和迭代器簡單高效,它們直接表達了操作,沒有額外的方法調用。使用默認操作符的函數是通用的。它可以用於支持該操作的任何類型。

缺點:

沒法通過閱讀方法名來區分對象的類型(例如has_key()意味着字典)。不過這也是優點。

結論:

如果類型支持,就使用默認迭代器和操作符,例如列表、字典和文件。內建類型也定義了迭代器方法。優先考慮這些方法,而不是那些返回列表的方法。當然,這樣遍歷容器時,你將不能修改容器。

for key in adict: ...
if key not in adict: ...
if obj in alist: ...
for line in afile: ...
for k, v in dict.iteritems(): ...

生成器

按需使用生成器.

定義:

所謂生成器函數,就是每當它執行一次生成(yield)語句,它就返回一個迭代器,這個迭代器生成一個值。生成值後,生成器函數的運行狀態將被掛起,直到下一次生成。

優點:

簡化代碼,因爲每次調用時,局部變量和控制流的狀態都會被保存。比起一次創建一系列值的函數, 生成器使用的內存更少。

缺點:

沒有。

結論:

鼓勵使用。注意在生成器函數的文檔字符串中使用”Yields:”而不是”Returns:”。

Lambda表達式

適用於單行函數

定義:

與語句相反,lambda在一個表達式中定義匿名函數。常用於爲map()filter()之類的高階函數定義回調函數或者操作符。

優點:

方便。

缺點:

比本地函數更難閱讀和調試。沒有函數名意味着堆棧跟蹤更難理解。由於lambda函數通常只包含一個表達式,因此其表達能力有限。

結論:

適用於單行函數。如果代碼超過60-80個字符,最好還是定義成常規(嵌套)函數。

對於常見的操作符,例如乘法操作符,使用operator模塊中的函數以代替lambda函數. 例如, 推薦使用operator.mul, 而不是lambda x, y: x * y.

條件表達式

適用於單行函數。

定義:

條件表達式是對於if語句的一種更爲簡短的句法規則。例如:x = 1 if cond else 2

優點:

比if語句更加簡短和方便。

缺點:

比if語句難於閱讀。如果表達式很長,難於定位條件。

結論:

適用於單行函數。在其他情況下,推薦使用完整的if語句。

默認參數值

適用於大部分情況。

定義:

你可以在函數參數列表的最後指定變量的值,例如def foo(a, b = 0):。如果調用foo時只帶一個參數,則b被設爲0。如果帶兩個參數,則b的值等於第二個參數。

優點:

你經常會碰到一些使用大量默認值的函數,但偶爾(比較少見)你想要覆蓋這些默認值。默認參數值提供了一種簡單的方法來完成這件事,你不需要爲這些罕見的例外定義大量函數。同時,Python也不支持重載方法和函數,默認參數是一種”仿造”重載行爲的簡單方式。

缺點:

默認參數只在模塊加載時求值一次。如果參數是列表或字典之類的可變類型,這可能會導致問題。如果函數修改了對象(例如向列表追加項),默認值就被修改了。

結論:

鼓勵使用。不過有如下注意事項:

不要在函數或方法定義中使用可變對象作爲默認值。

def foo(a, b=None):
    if b is None:
        b = []

屬性(properties)

訪問和設置數據成員時,你通常會使用簡單,輕量級的訪問和設置函數。建議用屬性(properties)來代替它們。

定義:

一種用於包裝方法調用的方式。當運算量不大,它是獲取和設置屬性(attribute)的標準方式。

優點:

通過消除簡單的屬性(attribute)訪問時顯式的get和set方法調用,可讀性提高了。允許懶惰的計算。用Pythonic的方式來維護類的接口。就性能而言,當直接訪問變量是合理的,添加訪問方法就顯得瑣碎而無意義。使用屬性(properties)可以繞過這個問題。將來也可以在不破壞接口的情況下將訪問方法加上。

缺點:

屬性(properties)是在get和set方法聲明後指定,這需要使用者在接下來的代碼中注意:set和get是用於屬性(properties)的(除了用property裝飾器創建的只讀屬性)。必須繼承自object類。可能隱藏比如操作符重載之類的副作用.。繼承時可能會讓人困惑。

結論:

你通常習慣於使用訪問或設置方法來訪問或設置數據,它們簡單而輕量。不過我們建議你在新的代碼中使用屬性。只讀屬性應該用property裝飾器來創建。

如果子類沒有覆蓋屬性,那麼屬性的繼承可能看上去不明顯。因此使用者必須確保訪問方法間接被調用,以保證子類中的重載方法被屬性調用(使用模板方法設計模式)。

Ture、False的求值

儘可能使用隱式false。

定義:

Python在布爾上下文中會將某些值求值爲false。按簡單的直覺來講,就是所有的”空”值都被認爲是false。因此0,None,[],{},“” 都被認爲是false。

優點:

使用Python布爾值的條件語句更易讀也更不易犯錯。大部分情況下,也更快。

缺點:

對C/C++開發人員來說。可能看起來有點怪。

結論:

儘可能使用隱式的false,例如:使用if foo:而不是if foo != []:。不過還是有一些注意事項需要你銘記在心:

  1. 永遠不要用==或者!=來比較單件,比如None。使用is或者is not。

  2. 當你寫下if x:時,你其實表示的是if x is not None。例如:當你要測試一個默認值是None的變量或參數是否被設爲其它值。這個值在布爾語義下可能是false!

  3. 永遠不要用==將一個布爾量與false相比較。使用if not x:代替。如果你需要區分false和None,你應該用像if not x and x is not None:這樣的語句。

  4. 對於序列(字符串、列表、元組),要注意空序列是false。因此if not seq:或者if seq:if len(seq):if not len(seq): 要更好。

  5. 處理整數時,使用隱式false可能會得不償失(即不小心將None當做0來處理)。你可以將一個已知是整型(且不是len()的返回結果)的值與0比較。

  6. 注意‘0’(字符串)會被當做true。

過時的語言特性

儘可能使用字符串方法取代字符串模塊。使用函數調用語法取代apply()。使用列表推導,for循環取代filter(),map()以及reduce()。

定義:

當前版本的Python提供了大家通常更喜歡的替代品。

結論:

我們不使用不支持這些特性的Python版本,所以沒理由不用新的方式。

此法作用域(Lexical Scoping)

推薦使用。

定義:

嵌套的Python函數可以引用外層函數中定義的變量,但是不能夠對它們賦值。變量綁定的解析是使用詞法作用域,也就是基於靜態的程序文本。對一個塊中的某個名稱的任何賦值都會導致Python將對該名稱的全部引用當做局部變量,甚至是賦值前的處理。如果碰到global聲明,該名稱就會被視作全局變量。

優點:

通常可以帶來更加清晰,優雅的代碼。尤其會讓有經驗的Lisp和Scheme(還有Haskell、ML等)程序員感到欣慰。

缺點:

可能導致讓人迷惑的bug。

結論:

鼓勵使用。

函數與方法裝飾器

如果好處很顯然, 就明智而謹慎的使用裝飾器

定義:

用於函數及方法的裝飾器(也就是@標記)。最常見的裝飾器是@classmethod 和@staticmethod,用於將常規函數轉換成類方法或靜態方法。不過,裝飾器語法也允許用戶自定義裝飾器。特別地,對於某個函數my_decorator,下面的兩段代碼是等效的:

class C(object):
   @my_decorator
   def method(self):
       # method body ... 
class C(object):
    def method(self):
        # method body ... 
    method = my_decorator(method)
優點:

優雅的在函數上指定一些轉換。該轉換可能減少一些重複代碼,保持已有函數不變(enforce invariants)等.

缺點:

裝飾器可以在函數的參數或返回值上執行任何操作,這可能導致讓人驚異的隱藏行爲。而且,裝飾器在導入時執行。從裝飾器代碼的失敗中恢復更加不可能。

結論:

如果好處很顯然,就明智而謹慎的使用裝飾器。裝飾器應該遵守和函數一樣的導入和命名規則。裝飾器的python文檔應該清晰的說明該函數是一個裝飾器。請爲裝飾器編寫單元測試。

避免裝飾器自身對外界的依賴(即不要依賴於文件、socket、數據庫連接等),因爲裝飾器運行時這些資源可能不可用(由pydoc或其它工具導入)。應該保證一個用有效參數調用的裝飾器在所有情況下都是成功的。

裝飾器是一種特殊形式的”頂級代碼”。

線程

不要依賴內建類型的原子性。

雖然Python的內建類型例如字典看上去擁有原子操作,但是在某些情形下它們仍然不是原子的(即: 如果hasheq被實現爲Python方法)且它們的原子性是靠不住的。你也不能指望原子變量賦值(因爲這個反過來依賴字典)。

優先使用Queue模塊的Queue數據類型作爲線程間的數據通信方式.。另外,使用threading模塊及其鎖原語(locking primitives)。瞭解條件變量的合適使用方式,這樣你就可以使用threading.Condition來取代低級別的鎖了。

威力過大的特性

避免使用這些特性。

定義:

Python是一種異常靈活的語言,它爲你提供了很多花哨的特性,諸如元類(metaclasses),字節碼訪問,任意編譯(on-the-fly compilation),動態繼承,對象父類重定義(object reparenting),導入黑客(import hacks),反射、系統內修改(modification of system internals)等等。

優點:

強大的語言特性,能讓你的代碼更緊湊。

缺點:

使用這些很”酷”的特性十分誘人,但不是絕對必要。使用奇技淫巧的代碼將更加難以閱讀和調試。開始可能還好(對原作者而言),但當你回顧代碼,它們可能會比那些稍長一點但是很直接的代碼更加難以理解。

結論:

在你的代碼中避免這些特性。

標準的模版庫和類在內部使用這些功能都還可以使用(列如:abc.ABCMetacollections.namedtupleenum)。

現代Python:Python 3和from__future__import{#modern-python}

Python3的時代。雖然不是每個項目都準備好但使用它,但是所有的代碼應該面向未來的眼光來寫。

定義:

Python 3 是一個很重要的改變在Python語言中,雖然現有的代碼通常是以2.7的形式編寫的,但爲了使代碼更明確地表達其意圖,需要做一些簡單的事情,從而更好地準備好在Python 3下使用而無需進行修改。

優點:

使用Python 3編寫的代碼在您的項目的所有依賴項都準備就緒後,會更加明確,並且更容易在Python 3下運行。

缺點:

有些人覺得額外的樣例很醜。 其他人說,“但我不在這個文件中使用該功能”,並希望清理。 請不要。 在所有文件中始終保留未來的導入效果會更好,以便在稍後進行編輯時,在有人開始使用此功能時不會忘記它們。

結論:

from__future__import

鼓勵使用from future import語句。所有新代碼應包含以下內容,並且應儘可能更新現有代碼以使其兼容:

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

還有其他from__future__語句。 如果您認爲合適,請使用它們。 我們不建議使用unicode_literals,因爲它在Python 2.7中的許多地方引入了隱式默認編解碼器轉換後果,因此它不是一個明顯的勝利。 大多數代碼在顯式使用b''u''字節和必要的unicode字符串文字時效果更好。

The six、future or past libraries.

當項目需要在Python 2和Python 3下積極支持使用時,在合適的情況下,鼓勵使用這些庫。 它們的存在是爲了讓您的代碼更簡潔,更輕鬆。

類型標註的代碼

你可以通過PEP-484標準註釋Python 3的類型提示,並且在構建時使用類型檢查工具(比如pytype)檢查代碼。

類型註釋可以在源代碼或stub pyi file中。 只要有可能,註釋應該在源代碼中。 爲第三方或擴展模塊使用pyi文件。

定義:

類型註釋(或者“類型提示”)用於函數或方法參數和返回值:

def func(a: int) -> List[int]:

你還可以使用特殊註釋聲明變量的類型:

= SomeFunc()  # type: SomeType 
優點:

類型註釋提高了代碼的可讀性和可維護性。類型檢查器會將許多運行時錯誤轉換爲構建時錯誤,並降低你使用強大特性的能力。

缺點:

你將不得不保持類型聲明是最新的。你有可能會看到你認爲是有效代碼的類型錯誤的。用代碼檢查器可以降低你使用強大特性的的能力。

結論:

這很大程度上取決於您項目的複雜程度。 試一試。

Python風格規範

分號

不要在行尾加分號,也不要用分號將兩條命令放在同一行。

行長度

每行長度不超過80字符。包括長的導入模塊語句、註釋裏的URL等。 不要使用反斜杆連接行。Python會將圓括號、中括號和花括號中的行隱式連接起來,可以利用這個特點,如果需要連接可以在表達式外圍增加一對額外的圓括號。 注:如果有必要,將長的URL可以放在一行。

括號

寧缺毋濫的使用括號。

除非是在使用實現行連接,否則不要在返回語句或條件語句中使用括號。不過在元組兩邊是可以的。

縮進

用四個空格來縮進代碼。

絕對不要用Tab,也不要用tab和空格混用。對於行連接的情況,要麼垂直對其換行的元素,或者使用4空格的懸掛式縮進。

空行

頂級定義之間空兩行,方法定義之間空一行。

頂級定義之間空兩行,比如函數或者類定義。方法定義,類定義與第一個方法之間,都應該空一行. 函數或方法中,某些地方要是你覺得合適,就空一行。

空格

按照標準的排版規範來使用標點兩邊的空格。

  • 括號內不要使用空格。
  • 不要在逗號、分號、冒號前面加空格,但應該在它們後面加(除了行尾)。
  • 參數列表、索引或切片的做括號前不應該加空格。
  • 在二元操作符兩邊都應該加上一個空格,至於算術操作符兩邊的空格該如何使用,需要你自己好好判斷。不過兩側務必要保持一致。
  • 當’=’用於指示關鍵字或參數默認值時,不要在其兩側使用空格。
  • 不要用空格來處置對齊多行簡的標記。

Shebang

大部分.py文件不必以#!作爲文件的開始。但是程序的main文件應該以 #!/usr/bin/python2或者#!/usr/bin/python3開始。

註釋

確保對模塊、函數,方法和行內註釋使用正確的風格。

文檔字符串

Python有一種獨一無二的註釋方式:使用文檔字符串。文檔字符串是包、模塊,類或函數的一個語句。這些字符串可以通過對象的__doc__成員被自動提取,並用被pydoc所用。對文檔字符串的慣例是使用三重雙引號”“”。 一個文檔字符串應該這樣組織:

  • 首先是一行以問號、句號和感嘆號結尾的概述。
  • 接着是一個空行。
  • 接着是文檔字符串剩下的部分,它應該與文檔字符串的第一行的第一個引號對齊。
模塊

每個文件應該包含一個許可樣板,根據項目使用許可(例如:Apache 2.0、BSD、LGPL、GPL),選擇合適的樣板。

函數和方法

下文所指的函數包括函數、方法以及生成器。

一個函數必須要有文檔字符串,除非它滿足以下條件:

  1. 外部不可見
  2. 非常短小
  3. 簡單明瞭 文檔字符串應該包含函數做什麼,以及輸入和輸出的詳細描述。通常,不應該描述“怎麼做”,除非是一些複雜的算法,文檔字符串應該提供足夠的信息,當別人編寫代碼調用該函數時,他不需要看一行代碼,只要看文檔字符串就可以了,對於複雜的代碼,在代碼旁邊加註釋會比使用文檔字符串更有意義。

關於函數的幾個方面應該在特定的小節中進行描述記錄,這幾個方面如下文所述。每節應該以一個標題行開始。標題行以冒號結尾。除標題行外、節的其他內容應被縮進2個空格。

Args: 列出每個參數的名字,並在名字後使用一個冒號和一個空格,分隔對該參數的描述。如果描述太長超過了單行80字符,使用2或者4個空格的懸掛縮進(與文件其他部分保持一致)。描述應該包括所需的類型和含義。如果一個函數接受foo(可變長度參數列表)或者**bar(任意關鍵字參數),應該詳細列出foo和bar。 Returns(或者Yield:用於生成器): 描述返回值的類型和語義。如果函數返回None,這一部分可以省略。 Raises:** 列出與接口有關的所有異常。

def fetch_bigtable_rows(big_tablekeysother_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.
    """
    pass

類應該在其定義下有一個用於描述該類的文檔字符串。如果你的類有公共屬性(Attributes),那麼文檔中應該有一個屬性(Attributes)段。並且應該遵守和函數參數相同的格式。

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__(selflikes_spam=False):
        """Inits SampleClass with blah."""
        self.likes_spam = likes_spam
        self.eggs = 0
 
    def public_method(self):
        """Performs operation blah."""
塊註釋和行註釋

最需要寫註釋的是代碼中那些技巧性的部分。如果你在下次代碼審查的時候必須解釋一下,那麼你應該現在就給它寫註釋。對於複雜的操作,應該在其操作開始前寫上若干行註釋。對於不是一目瞭然的代碼,應在其行尾添加註釋。

# 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 iff i is a power of 2 
  • 爲了提高可讀性,註釋應該至少離開代碼2個空格。
  • 另一方面,絕不要描述代碼。假設閱讀代碼的人比你更懂Python,他只是不知道你的代碼要做什麼。
# BAD COMMENT: Now go through the b array and make sure whenever i occurs 
# the next element is i+1 

如果一個類不繼承自其它類,就顯式的從object繼承,嵌套類也一樣。

class SampleClass(object):
         pass
 
 
class OuterClass(object):
 
    class InnerClass(object):
             pass
 
 
class ChildClass(ParentClass):
         """Explicitly inherits from another class already."""

繼承自object是爲了使屬性(properties)正常工作並且這樣可以保護你的代碼,使其不受PEP-3000的一個特殊的潛在不兼容性影響。這樣做也定義了一些特殊的方法,這些方法實現了對象的默認語義。包括__new__,__init__, __delattr__,__getattribute__,__setattr__,__hash__,__repr__and __str__

字符串

即使參數都是字符串,使用%操作符或者格式化方法格式化字符串。不過也不能一概而論,你需要在+和%之間好好判定。

= a + b
= '%s%s!' % (imperative,expletive)
= '{}{}!'.format(imperative,expletive)
= 'name: %s; score: %d' % (name,n)
= 'name: {}; score: {}'.format(name,n)

避免在循環中用+和+=操作符來累加字符串。由於字符串是不可變的,這樣做會創建不必要的臨時對象,並且導致二次方而不是線性的運行時間。作爲替代方案,你可以將每個子串加入列表,然後在循環結束後用.join連接列表。(也可以將每個子串寫入一個 .cStringIO.StringIO緩存中。)

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)

在同一個文件中,保持使用字符串引號的一致性。使用單引號’或者雙引號”之一用以引用字符串,並在同一文件中沿用。在字符串內可以使用另外一種引號,以避免在字符串中使用。PyLint已經加入了這一檢查。

Python('Why are you hiding your eyes?')
Gollum("I'm scared of lint errors.")
Narrator('"Good!" thought a happy Python reviewer.')

爲多行字符串使用三重雙引號”“”而非三重單引號”’。當且僅當項目中使用單引號’來引用字符串時,,纔可能會使用三重”’爲非文檔字符串的多行字符串來標識引用。文檔字符串必須使用三重雙引號”“”。不過要注意,通常用隱式行連接更清晰,因爲多行字符串與程序其他部分的縮進方式不一致。

print ("This is much nicer.\n"
       "Do it this way.\n")

文件和sockets

在文件和sockets結束時,顯式的關閉它。

除文件外,sockets或其他類似文件的對象在沒有必要的情況下打開,會有許多副作用,例如:

  1. 它們可能會消耗有限的系統資源,如文件描述符。如果這些資源在使用後沒有及時歸還系統,那麼用於處理這些對象的代碼會將資源消耗殆盡。
  2. 持有文件將會阻止對於文件的其他諸如移動、刪除之類的操作。
  3. 僅僅是從邏輯上關閉文件和sockets,那麼它們仍然可能會被其共享的程序在無意中進行讀或者寫操作。只有當它們真正被關閉後,對於它們嘗試進行讀或者寫操作將會拋出異常,並使得問題快速顯現出來。

而且,幻想當文件對象析構時,文件和sockets會自動關閉,試圖將文件對象的生命週期和文件的狀態綁定在一起的想法,都是不現實的。因爲有如下原因:

  1. 沒有任何方法可以確保運行環境會真正的執行文件的析構。不同的Python實現採用不同的內存管理技術,如延時垃圾處理機制。延時垃圾處理機制可能會導致對象生命週期被任意無限制的延長。
  2. 對於文件意外的引用,會導致對於文件的持有時間超出預期(比如對於異常的跟蹤,包含有全局變量等)。 推薦使用"with"語句以管理文件:
    with open("hello.txt") as hello_file:
     for line in hello_file:
         print line
    對於不支持使用”with”語句的類似文件的對象,使用contextlib.closing(): “`python import contextlib

with contextlib.closing(urllib.urlopen(“http://www.python.org/“)) as front_page: for line in front_page: print line

 
#### TODO註釋
爲臨時代碼使用TODO註釋,它是一種短期解決方案。不算完美,但夠好了。
 
TODO註釋應該在所有開頭處包含”TODO”字符串,緊跟着是用括號括起來的你的名字,email地址或其它標識符。然後是一個可選的冒號,接着必須有一行註釋,解釋要做什麼。主要目的是爲了有一個統一的TODO格式。這樣添加註釋的人就可以搜索到(並可以按需提供更多細節)。寫了TODO註釋並不保證寫的人會親自解決問題。當你寫了一個TODO,請註上你的名字。
```python
TODO([email protected]): Use a "*" here for string repetition.
TODO(Javy) Change this to use relations.

如果你的TODO是”將來做某事”的形式,那麼請確保你包含了一個指定的日期(“2018年3月解決”)或者一個特定的事件(“等到所有的客戶都可以處理XML請求就移除這些代碼”)。

#### 導入格式
每個導入應該獨佔一行。
```python
import os
import sys

導入總應該放在文件頂部,位於模塊註釋和文檔字符串之後,模塊全局變量和常量之前。導入應該按照從最通用到最不通用的順序分組:

  1. 標準庫導入
  2. 第三方庫導入
  3. 應用程序指定導入 每種分組中,應該根據每個模塊的完整包路徑按字典序排序,忽略大小寫。
    import foo
    from foo import bar
    from foo.bar import baz
    from foo.bar import Quux
    from Foob import ar

    語句

    通常每個語句應該獨佔一行。

不過,如果測試結果與測試語句在一行放得下,你也可以將它們放在同一行。如果是if語句,只有在沒有else時才能這樣做。特別地,絕不要對y/except這樣做,因爲try和except不能放在同一行。

訪問控制

在Python中,對於瑣碎又不太重要的訪問函數,你應該直接使用公有變量來取代它們,這樣可以避免額外的函數調用開銷。當添加更多功能時,你可以用屬性(property)來保持語法的一致性。

另一方面,如果訪問更復雜,或者變量的訪問開銷很顯著,那麼你應該使用像get_foo()set_foo()這樣的函數調用。如果之前的代碼行爲允許通過屬性(property)訪問,那麼就不要將新的訪問函數與屬性綁定。這樣,任何試圖通過老方法訪問變量的代碼就沒法運行,使用者也就會意識到複雜性發生了變化。

命名

module_name,package_name,ClassName,method_name,ExceptionName,function_name,GLOBAL_VAR_NAME,instance_var_name,function_parameter_name,local_var_name。

應該避免的名字
  1. 單字符名稱, 除了計數器和迭代器。
  2. 包/模塊名中的連字符(-)。
  3. 雙下劃線開頭並結尾的名稱(Python保留,例如init)。
    命名約定
  4. 所謂”內部(Internal)”表示僅模塊內可用,或者在類內是保護或私有的。
  5. 用單下劃線(_)開頭表示模塊變量或函數是protected的(使用import * from時不會包含)。
  6. 用雙下劃線(__)開頭的實例變量或方法表示類內私有。
  7. 將相關的類和頂級函數放在同一個模塊裏。不像Java,沒必要限制一個類一個模塊。
  8. 對類名使用大寫字母開頭的單詞(如CapWords, 即Pascal風格),但是模塊名應該用小寫加下劃線的方式(如lower_with_under.py)。儘管已經有很多現存的模塊使用類似於CapWords.py這樣的命名,但現在已經不鼓勵這樣做,因爲如果模塊名碰巧和類名一致,這會讓人困擾。
Python之父Guido推薦的規範
Type Public Internal
Modules lower_with_under _lower_with_under
Packages lower_with_under
Classes CapWords _CapWords
Exceptions CapWords
Functions lower_with_under() _lower_with_under()
Global/Class Constants CAPS_WITH_UNDER _CAPS_WITH_UNDER
Global/Class Variables lower_with_under _lower_with_under
Instance Variables lower_with_under _lower_with_under (protected) or __lower_with_under (private)
Method Names lower_with_under() _lower_with_under() (protected) or __lower_with_under() (private)
Function/Method Parameters lower_with_under
Local Variables lower_with_under

Main

即使是一個打算被用作腳本的文件,也應該是可導入的。並且簡單的導入不應該導致這個腳本的主功能(main functionality)被執行,這是一種副作用。主功能應該放在一個main()函數中。

在Python中,pydoc以及單元測試要求模塊必須是可導入的。你的代碼應該在執行主程序前總是檢查if __name__ == '__main__',這樣當模塊被導入時主程序就不會被執行。

def main():
      ...
 
if __name__ == '__main__':
    main()

所有的頂級代碼在模塊導入時都會被執行。要小心不要去調用函數,創建對象,或者執行那些不應該在使用pydoc時執行的操作。

函數長度

喜歡小而專注的函數。

我們知道長函數有時是合適的,所以對函數長度沒有硬性限制。 如果功能超過40行,請考慮是否可以在不影響程序結構的情況下將其分解。

即使長函數依然可以正常工作,但有人在幾個月內修改它可能會增加新的行爲。這可能會導致很難找到的錯誤。保持你函數的簡短,使其他人更容易閱讀和修改你的代碼。

使用某些代碼時,可能會發現長而複雜的函數。不要被修改現有代碼而嚇倒:如果使用這樣的函數證明是困難的,你會發現錯誤是很難調試發現的,或者你想在幾個不同的上下文中使用它的一部分,考慮把函數分解成更小的、更易於管理的函數。

類型註釋

普通規則
  • 熟悉PEP-484.
  • 在方法中,永遠不要註釋self或cls。
  • 如果不應表達任何其他變量或返回類型,請使用Any。
  • 你不需要註釋模塊中的所有功能。
    • 至少註釋你的公共API。
    • 使用判斷力一方面可以在安全性和清晰度之間取得良好的平衡,另一方面可以靈活應對。
    • 註釋容易出現類型相關錯誤的代碼(以前的錯誤或複雜性)。
    • 註釋難以理解的代碼。
    • 註釋代碼從類型角度來看變得穩定。在很多情況下,你可以在成熟代碼中註釋所有功能,而不會失去太多的靈活性。

斷行

嘗試遵循現有的縮進規則。 總是喜歡在各種變量之間斷開。

註釋後,許多功能將成爲“每行一個參數”。

def my_method(self,
              first_var: int,
              second_var: Foo,
              third_var: Optional[Bar]) -> int:
  ...

但是,如果所有東西都放在一行,那也是合適的。

def my_method(self, first_var: int) -> int:
  ...

如果函數名稱,最後一個參數和返回類型的組合太長,則在新行中縮進4。

def my_method(
    self, first_var: int) -> Tuple[MyLongType1, MyLongType1]:

當返回類型與最後一個參數不在同一行時,首選方法是將參數縮進4行,並將右括號與def對齊。

def my_method(
    self, **kw_args: Optional[MyLongType]
) -> Dict[OtherLongTypeMyLongType]:
  ...

pylint允許你將右括號移動到一個新的行並與開頭的對齊,但這是可讀較差,如下所示。

def my_method(self,
              **kw_args: Optional[MyLongType]
             ) -> Dict[OtherLongTypeMyLongType]:
  ...

正如在上面的例子中,不要打破類型。但是,有時它們太長而無法在一行上(儘量保持子類型不間斷)。

def my_method(
    self,
    first_var: Tuple[List[MyLongType1],
                     List[MyLongType2]],
    second_var: List[Dict[
        MyLongType3, MyLongType4]]) -> None:
  ...

如果單個名稱和類型太長,請考慮爲該類型使用別名。最後的手段是斷開冒號和縮進四行。

def my_function(
    long_variable_name:
        long_module_name.LongTypeName,
) -> None:
前置聲明

如果需要使用尚未定義的相同模塊中的類名-例如,如果需要類聲明中的類,或者使用了下面定義的類 - 請爲該類使用字符串名稱。

class MyClass(object):
 
  def __init__(self,
               stack: List["MyClass"]) -> None:
默認值

根據PEP-008將參數註釋與默認值相結合,在=號周圍使用空格(但僅限於那些同時具有註釋和默認值的參數)。

def func(a: int = 0) -> int:
  ...
NoneType

在Python類型系統中,NoneType是“第一類”類型,出於輸入的目的,NoneNoneType的別名。如果參數可以是None,則必須聲明!你可以使用Union,但如果只有一個其他類型,則Optional是一個快捷方式。

def func(a: Optional[str]) -> str:
...

如果參數的默認值爲None,則標記變量Optional是可選的。

def func(a: Optional[str] = None) -> str:
  ...
def func(a: str = None) -> str:
  ...
類型別名

你可以爲複雜類型聲明別名。別名的名稱應該是CapWorded; 嘗試描述組合類型並以“Type”(或返回元組的“類型”)結束。如果該別名僅在本模塊中使用,則應該是_Private。

例如,如果模塊名稱與類型一起太長: SomeType = module_with_long_name.TypeWithLongName

其他例子是複雜的嵌套類型和函數的多個返回變量(作爲元組)。

忽略類型

你可以使用特殊註釋#type:ignore禁用類型檢查。

pytype對特定的錯誤有一個禁用選項(類似於lint):

# pytype: disable=attribute-error 
鍵入內部變量

如果內部變量的類型很難或不可能推斷,那麼可以將其作爲特殊評論提供: a = SomeUndecoratedFunction() # type: Foo

元組和列表

與僅有單一類型的列表不同,元組可以具有單個重複類型或具有不同類型的一定數量的元素。後者通常用作函數的返回類型。

= [1, 2, 3]  # type: List[int] 
= (123)  # type: Tuple[int, ...] 
= (1"2"3.5)  # type Tuple[int, str, float] 
TypeVar

Python類型系統具有generics。 工廠函數TypeVar是使用它們的常用方法。 例如:

from typing import List, TypeVar
= TypeVar("T")
...
def next(l: List[T]) -> T:
  return l.pop()

TypeVar可以被約束:

AddableType = TypeVar("AddableType", int, float, str)
def add(a: AddableType, b: AddableType) -> AddableType:
  return a + b

類型變量中一個常見的預定義類型變量是AnyStr。 將它用於可以是bytesunicode的參數。

AnyStr = TypeVar("AnyStr", bytes, unicode)
字符串類型

當註釋接收或返回字符串的函數時,避免使用str,因爲它在Python 2和Python 3中意味着不同的東西。在Python 2中,strbytes; 在Python 3中,它是unicode。 只要有可能,最好是明確的.

對於處理字節數組的代碼,請使用bytes

def f(x: bytes) -> bytes:
  ...

對於處理Unicode數據的代碼,請使用Text

from typing import Text
...
def f(x: Text) -> Text:
  ...

如果類型可以是字節或unicode,則使用Union

from typing import Text, Union
...
def f(x: Union[bytes, Text]) -> Union[bytesText]:
  ...

如果函數的所有字符串類型始終相同,例如,如果返回類型與上面代碼中的參數類型相同,請使用AnyStr

以這種方式編寫代碼將簡化將代碼移植到Python 3的過程。

類型導入

對於來自typing模塊的類,始終導入類本身。明確允許從typing模塊的一行中導入多個特定的類。例如:

from typing import Any, Dict, Optional

鑑於這種typing的方式會將項添加到本地名稱空間,所以導入中的任何名稱都應該與關鍵字類似,並且不能在您的Python代碼中定義,無論是否導入。如果模塊中的類型和現有名稱之間存在衝突,請使用import x as y

from typing import Any as AnyType

如果需要在運行時避免類型檢查所需的附加導入,則可以使用條件導入。不鼓勵這種模式,並且應該優先選擇重構代碼以允許頂級導入的替代方案。如果完全使用這種模式,那麼有條件地導入的類型需要被引用爲字符串’sketch.Sketch'而不是sketch.Sketch,以與Python 3向前兼容,其中註釋表達式被實際評估。 只有type註釋才需要的導入可以放在一個if typing.TYPE_CHECKING:塊中。

  • 應該在這裏定義僅用於設置的實體;這包括別名。否則,它將成爲運行時錯誤,因爲模塊不會在運行時導入。
  • 塊應該在所有正常導入。
  • 類型導入列表中不應該有空行。
  • 將列表排序,就好像它是一個常規導入列表一樣,但是將末尾的輸入模塊導入。
  • google3模塊也有一個TYPE_CHECKING常量。 如果你不想在運行時typing輸入,你可以使用它。
    import typing
    ...
    if typing.TYPE_CHECKING:
    import types
    from MySQLdb import connections
    from google3.path.to.my.project import my_proto_pb2
    from typing import Any, Dict, Optional
循環依賴

由鍵入代碼的循環依賴。這樣的代碼是重構的好選擇。 儘管技術上可以保持循環依賴,但構建系統不會讓你這麼做,因爲每個模塊都必須依賴於其他模塊。

使用Any替換創建循環依賴項導入的模塊。設置有意義的名稱用別名,並使用此模塊中的實際類型名稱(任何屬性均爲Any)。 別名定義應該與上一次導入分開一行。

from typing import Any
 
some_mod = Any  # some_mod.py imports this module. 
...
 
def my_method(self, var: some_mod.SomeType) -> None:
  ...

總結

請務必保持代碼的一致性

如果你正在編輯代碼, 花幾分鐘看一下週邊代碼, 然後決定風格. 如果它們在所有的算術操作符兩邊都使用空格, 那麼你也應該這樣做。如果它們的註釋都用標記包圍起來, 那麼你的註釋也要這樣。

制定風格指南的目的在於讓代碼有規可循, 這樣人們就可以專注於”你在說什麼”, 而不是”你在怎麼說”. 我們在這裏給出的是全局的規範, 但是本地的規範同樣重要. 如果你加到一個文件裏的代碼和原有代碼大相徑庭, 它會讓讀者不知所措。 避免這種情況。

注:

原文來自於Google PyGuid

參考譯文來自於Google Python Style Guide

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章