Python程式設計風格和設計模式

-柚子皮-發表於2014-05-24

http://blog.csdn.net/pipisorry/article/details/26840461

the Zen of Python Python的禪學

>>> import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

模式(Patterns)

“請求寬恕比請求許可更容易(EFAP)”

這個Python設計原則是這麼說的“請求寬恕比請求許可更容易(EFAP)”。不提倡深思熟慮的設計思路,這個原則是說應該儘量去嘗試,如果遇到錯誤,則給予妥善的處理。Python有著強大的異常處理機制可以支援這種嘗試,這些機制幫助程式設計師開發出更為穩定,容錯性更高的程式。

單例

單例是指只能同時存在一個的例項物件。Python提供了很多方法來實現單例。

Null物件

Null物件能夠用來代替None型別以避免對None的測試。

觀察者

觀察者模式允許多個物件訪問同一份資料。

建構函式

建構函式的引數經常被賦值給例項的變數。這種模式能夠用一行程式碼替代多個手動賦值語句。


Python 程式設計風格指南

針對Python Style Guide Jun 18, 2009 版本翻譯。本文是向 Melange 專案貢獻 Python 程式碼的編碼風格指南。

SoC framework專案以及在此之上構建的Melange web applications都是用 Python 語言實現的(因為 Python 是Google App Engine目前支援的唯一一種程式語言)。本編碼風格指南是向 Melange 專案貢獻 Python 程式碼的“要做”和“不要做”的列表。

下面這些規則不是原則或建議,而是嚴格的規定。你不能不理會這些規定除非是確實需要使用並被允許了,但仍然要注意本指南結尾處一致性部分的建議。

如果你有關於這些準則的問題,可以向開發者郵件列表發郵件詢問。請在郵件標題寫上“Python style:”,以便在郵件列表歸檔中更容易定位這些討論。

本編碼風格指南寫在 Melange 專案的 Wiki 上,可以遵循已有的文件複查流程?進行修改。但不應該輕易修改本文,因為一致性是本指南的一個關鍵目標,而且不能因為新的風格指南變化而需要更新舊程式碼。


1.  概述


1.1  Python 語言方面的準則

  1. pychecker: 建議使用
  2. 匯入模組和包: 可以,但不要 import *
  3. 完整路徑匯入: 可以
  4. 異常處理: 可以
  5. 全域性變數: 謹慎使用
  6. 內嵌/本地/內部類和函式: 可以
  7. List Comprehensions: 可以用,如果簡明易懂的話
  8. 預設迭代器和運算子: 可以
  9. 生成器: 可以
  10. 使用 apply、 filter、 map、 reduce: 對one-liner來說可以
  11. Lambda 函式: 對one-liner來說可以
  12. 預設引數值: 可以
  13. Properties: 可以
  14. True/False 求值: 可以
  15. 布林內建型別: 可以
  16. String 方法: 可以
  17. 靜態域: 可以
  18. 函式和方法修飾符:適度使用
  19. 執行緒:不要用Google App Engine不支援)
  20. 高階特性:不要用

1.2  Python 編碼風格方面的準則

所有檔案都保持一致風格時程式要容易維護得多,以下是 Melange 專案的標準 Python 編碼風格規範:

  1. 分號:避免使用
  2. 每行長度: 最多80
  3. 圓括號:吝嗇地用
  4. 縮排:4 空格(不要用 tab),PEP8有所區別
  5. 空行: 對函式和類用2 個空行分隔,對類的方法用1 個空行
  6. 空格: 在行內吝嗇地使用
  7. Python 直譯器: 用Google App Engine支援的那個: #!/usr/bin/python2.5
  8. 註釋:__doc__ strings、塊註釋、行內註釋
  9. : 繼承 object
  10. 字串: 避免重複使用+ 和+=
  11. TODO style:TODO(username): 使用一種一致的風格
  12. import 分組、排序和整理: 一行一個,按包名字分組順序放置,按字母順序排序
  13. 語句: 一行一個,避免使用分號
  14. 訪問控制: 輕量化風格可以用foo,否則用GetFoo() 和SetFoo()
  15. 命名: 用foo_bar.py 而不是foo-bar.py,和PEP8有些區別
  16. 程式入口:if __name__ == '__main__':
  17. 總結: 看看你周圍是什麼


2.  Python 語言方面的準則


2.1  pychecker

是什麼: pychecker 是從 Python 原始碼中找 Bug 的工具。類似 lint ,它尋找在 C 和 C++ 那樣更少動態化的語言中通常由編譯器捕捉的那些問題。因為 Python 天生就是動態化的,其中一些警告可能並不正確;但假警報應該是相當少見的。

優點: 捕捉易犯的錯誤,比如拼寫、在賦值之前就使用變數等等。

缺點:pychecker 不是完美的。為了好好利用它,我們有時候得:a) 針對它來寫程式碼 b) 禁用特定警告 c) 改進它 d) 或者忽略它。

決定: 確保在程式碼上執行pychecker

你可以設定一個名為 __pychecker__ 的模組級別變數來適當禁用某些警告。比如:

__pychecker__ = 'no-callinit no-classattr'

用這種方法禁用有個好處:我們可以很容易地找到這些禁用並且重新啟用它們。

你可以用 pychecker --help 來獲得 pychecker 警告的列表。

禁用“未被使用的變數”警告可以用 _ 作為未使用變數的識別符號,或者給變數名新增unused_ 字首。有些情況下無法改變變數名,那你可以在函式開頭呼叫它們一下,比如:

def Foo(a, unused_b, unused_c, d=None, e=None):
  d, e = (d, e)  # silence pychecker
  return a

最理想的是, pychecker 會竭盡全力保證這樣的“未使用宣告”是真實的。

你可以在PyChecker 主頁找到更多相關資訊。

2.2  匯入模組和包

是什麼: 在模組與模組之間共享程式碼的重用機制。

優點: 最簡單同時也是最常用的共用方法。

缺點:from foo import * 或者from foo import Bar 很噁心而且這會使尋找模組之間的依賴關係變難,從而導致嚴重的維護問題。

決定: 用import x 來匯入包和模組。只有在x 是一個包(package),而y 是一個模組(module)的時候才用from x import y 。這可以讓使用者無需說明完整的包字首就能引用模組。比如sound.effects.echo 包可以像下面這樣匯入:

from sound.effectsimport echo
...
echo.echofilter(input, output, delay=0.7, atten=4)

即使模組是在同一個包裡,也不要直接匯入模組而不給出完整的包名字。這可能導致包被匯入多次以及非預期的副作用。

例外的情況:當且僅當 Bar 是 foo 中的一個頂級 singleton,並且叫做 foo_Bar 是為了描述 Bar 與 foo 之間的關係,那麼才可以用from foo import Bar as foo_Bar 。

比如,像下面這樣是可以的:

from soc.logic.models.userimport logicas user_logic
...
user_logic.getForCurrentAccount()

以下寫法更受歡迎:

from soc.logicimport models
...
import soc.logic.models.user
...
models.logic.user.logic.getForCurrentAccount()

2.3  完整路徑匯入

是什麼: 每個模組都通過模組的完整路徑位置匯入和引用。

優點: 避免模組名字之間的衝突,而且查詢模組更容易。

缺點: 部署程式碼會麻煩一些,因為你必須重現整個包的分層結構。

決定: 所有新程式碼都應該使用他們的包名字來引用模組。不要為了從其他目錄匯入模組而修改sys.path 和PATHONPATH。(可能有些情況下仍然需要修改路徑,但只要可能的話就應該避免這樣做。)

應該像下面這樣匯入:

# 使用完整名字引用:
import soc.logging

# 僅使用模組名字引用:
from soc import logging

有些包名字可能和常用的本地變數名是一樣的。在這種情況下為了避免混亂,先匯入包然後清晰地單獨匯入模組有時候也是有意義的。結果看起來像這樣:

from soc import models
import soc.models.user

...
  user = models.user.User()

在建立時就避免易引發混亂的模組命名可能是最佳方案,但有時選擇直觀的模組名字(比如上面例子中 soc.models 中的那些模組)導致需要像上面這樣做 workaround 。

2.4  異常處理

是什麼: 異常處理指的是為處理錯誤及其他異常狀況而打破程式碼塊的正常控制流。

優點: 正常操作程式碼的控制流不會被錯誤處理程式碼弄亂。也可以在特定情況發生時讓控制流跳過多個步驟,比如一步就從 N 層巢狀函式(nested functions)中返回,而不必繼續把錯誤程式碼一步步執行到底。

缺點: 可能使控制流變得難以理解,以及在做函式庫呼叫時容易忽略一些錯誤情況。

決定: 異常處理是很 Pythonic 的,因此我們當然同意用它,但只是在符合以下特定條件時:

  • 要像這樣丟擲異常:raise MyException("Error message") 或者raiseMyException,而不要用雙引數的形式(raiseMyException, "Error message") 或者 已廢棄的基於字串的異常(raise "Error message")。
  • 模組和包應該定義自己的特定領域的基礎異常類,而且這個類應該繼承自內建的 Exception 類。這種用於一個模組的基礎異常應該命名為Error
    class Error(Exception):
      """Base exception for all exceptions raised in module Foo."""
      pass
  • 永遠不要直接捕獲所有異常,也即 except: 語句,或者捕獲 Exception 和 StandardError,除非你會把異常重新丟擲或者是在你執行緒最外層的程式碼塊中(而且你會列印一個出錯資訊)。在這種意義上來講, Python 是很健壯的,因為except: 會真正捕獲包括 Python 語法錯誤在內的所有東西。使用except: 很容易會掩蓋真正的 Bug 。
  • 在 try/except 塊中使程式碼量最小化。try 裡頭的內容越多,就更有可能出現你原先並未預期會丟擲異常的程式碼丟擲異常的情況。在這種情況下,try/except 程式碼塊就隱藏了真正的錯誤。
  • 使用 finally 子句執行一段程式碼而無論 try 語句塊是否會丟擲異常。這在做清除工作的時候經常是很有用的,比如關閉檔案。

2.5  全域性變數

是什麼: 在模組級別宣告的變數。

優點: 有時很有用。

缺點: 有可能在匯入時改變模組的行為,因為在模組被匯入時完成了模組級別變數的賦值。

決定: 推薦使用類變數而避免使用全域性變數。以下是一些例外情況:

  • 作為指令碼中的預設選項。
  • 模組級別常量,例如 PI = 3.14159 。常量名應該全部使用大寫字母並用下劃線分隔,參考下文命名章節。
  • 有時全域性變數對快取函式返回值或其他需要的運算結果會是挺有用的。
  • 在需要的時候,全域性變數應該被用作模組的內部變數,並通過公開的模組級函式來訪問。參考下文命名?章節。

2.6  內嵌/本地/內部類和函式

是什麼: 類可以定義在函式或另一個類的內部。函式可以定義在另一個函式的內部。內嵌函式可以只讀訪問定義在外部作用域中的變數。

優點: 允許定義只用於一個非常有限的作用域內部的工具類和工具函式,實在太方便了(ADT-y)。

缺點: 內嵌的本地類例項無法被序列化匯出(pickle)。

決定: 挺好的,用吧。

2.7  List Comprehensions

是什麼: List Comprehension 和 Generator Expression 提供了一種簡明高效的方法來建立列表和迭代器而無需藉助map() 、filter() 、或者lambda 。

優點: 簡單的 List Comprehensions 比其他列表建立技術更清晰、更簡單。Generator Expressions 非常高效,因為它避免了一下子建立整個列表。

缺點: 複雜的 List Comprehensions 或 Generator Expressions 很難讀懂。

決定: 對一行風格(one-liner)來說沒問題,或者在x for y in z 能寫在一行裡而條件語句可以寫成另一行的時候(注意下面x 很長的、寫成三行的那個例子是可以接受的)也可以用。當問題變得更復雜時應該用迴圈來代替。這是一種主觀判斷。

No:

# 太複雜了,應該用迴圈巢狀代替:
result = [(x, y)for xinrange(10)
                   for y in range(5)
                     if x * y > 10]

Yes:

# 這個例子足夠複雜了,*不應該*使用List Comprehension:
result = []
for x in range(10):
  for y in range(5):
    if x * y > 10:
      result.append((x, y))

# 簡單舒服的一行風格:
squares = [* x for x in range(10)]

# 寫成兩行的 Generator (不是 List Comprehension)例子,也是可以的:
Eat(jelly_bean for jelly_bean in jelly_beans
    if jelly_bean.color == 'black')

# 寫成三行的這個例子是否仍然比等價的迴圈具有更好的可讀性還存在爭議:
result = [someReallyLongWayToEatAJellyBean(jelly_bean)
          for jelly_bean in jelly_beans
          if jelly_bean !'black']

2.8  預設迭代器和運算子

是什麼: 容器型別(比如字典和列表)定義了預設迭代器和從屬關係測試運算子(例如in 和not in)。

優點: 預設迭代器和運算子又簡單又高效,它們無需額外方法呼叫就能直接表達操作。使用預設運算子來編寫函式是一種通用性較強的做法,因為它可以用於任何支援這些操作的資料型別。

缺點: 你無法通過閱讀方法名字就說出物件的型別(比如說has_key() 表示一個字典)。這同時也是一個優點。

決定: 在支援它們的型別上使用預設迭代器和運算子,比如列表、字典、和檔案。當然這些內建型別同時也定義了迭代器方法。對返回列表的方法傾向於使用迭代器方法,但你不應該在迭代的過程中修改容器中的內容。

Yes:

for key in adict: ...

if key not in adict: ...

if obj in alist: ...

for line in afile: ...

for k, v in dict.iteritems(): ...

No:

for key in adict.keys(): ...

if not adict.has_key(key): ...

for line in afile.readlines(): ...

2.9  生成器

是什麼: 生成器函式返回一個迭代器,而這個迭代器每次執行yield 語句生成一個計算結果。每次生成一個計算結果之後,生成器函式的執行時狀態被掛起,直到需要生成下一個計算結果時。

優點: 程式碼更簡單。因為對每次呼叫來說,本地變數和控制流的狀態都是被保留的。生成器比用計算結果一次性填滿整個列表的函式更節省記憶體。

缺點: 沒有。

決定: 很好。在生成器函式的__doc__ String 中使用"Yields:" 而不是"Returns:" 。

2.10  使用 apply filter map reduce

是什麼: 實用的內建列表操作函式。通常和lambda 函式一起用。

優點: 程式碼很緊湊。

缺點: 高階函數語言程式設計恐怕更難理解。

決定: 對簡單程式碼和喜歡把程式碼寫成一行的人來說,儘可能用 List Comprehension 而限制使用這些內建函式。一般來說,如果程式碼在任何地方長度超過60到80個字元,或者使用了多層函式呼叫(例如,map(lambda x: x[1], filter(...)))),那這就是一個訊號提醒你最好把它重新寫成一個普通的迴圈。比較:

map/filter:

map(lambda x:x[1],filter(lambda x:x[2] ==5, my_list))

list comprehensions:

[x[1]for xin my_listif x[2] ==5]

2.11  Lambda functions

是什麼: Lambda 定義僅包含表示式、沒有其他語句的匿名函式,通常用於定義回撥或者用於map() 和filter() 這樣高階函式的運算子。

優點: 方便。

缺點: 比本地函式更難閱讀和除錯。沒有名字意味著堆疊追蹤資訊更難理解。而且函式只能包含一個表示式,因而表達能力也比較有限。

決定: 對喜歡把程式碼寫成一行的人來說沒什麼問題。只要lambda 函式中的程式碼長度超過60到80個字元,那可能把它定義成一個標準(或者巢狀的)函式更合適。

對加法這樣的普通操作,應該使用 operator 模組中的函式而不是用 lambda 函式。例如,應該用operator.add 而不是lambda x, y: x + y。(內建的sum() 函式其實比這兩者中的任何一個都更合適。)

2.12  預設引數值

是什麼: 你可以給函式的引數列表中最靠後的幾個變數指定取值,比如def foo(a, b=0): 。如果只用一個引數來呼叫foo ,b 將被設定為0 。如果用兩個引數來呼叫它,b 將使用第二個引數的值。

優點: 通常你會大量使用函式的預設值,但偶爾會需要覆蓋這些值。預設引數值允許用一種簡單的辦法來做到這一點,而不必為偶爾的例外情形定義一大堆函式。而且, Python 不支援方法/函式過載,而引數預設值是“模仿”過載行為的一種簡單方法。

缺點: 預設引數值會在模組載入的時候被賦值。如果引數是一個列表或者字典之類的可變物件就有可能造成問題。如果函式修改了這個物件(比如在列表最後附加了一個新的項),那預設值也就改變了。

決定: 在以下限制條款下是可以使用的:

  • 不要把可變物件當作函式或方法定義的預設值。

Yes:

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

No:

def foo(a, b=[]):
  ...

呼叫函式的程式碼必須對預設引數使用指名賦值(named value)。這多少可以幫助程式碼的文件化,並且當增加新引數進來時幫助避免和發現破壞原有介面。

def foo(a, b=1):
  ...

Yes:

foo(1)
foo(1, b=2)

No:

foo(12)

2.13  Properties

是什麼: 在運算量很小時,把對屬性的 get 和 set 方法呼叫封裝為標準的屬性訪問方式的一個方法。

優點: 對簡單的屬性訪問來說,去掉直率的get 和set 方法呼叫提高了程式碼的可讀性。允許延後計算。考慮到 Pythonic 的方法來維護類的介面。在效能方面,當直接變數訪問是合理的,允許 Properties 就省略了瑣碎的屬性訪問方法,而且將來仍然可以在不破壞介面的情況下重新加入屬性訪問方法。

缺點: Properties 在 getter 和 setter 方法宣告之後生效,也就是要求使用者注意這些方法在程式碼很靠後的地方才能被使用(除了用@property 描述符建立的只讀屬性之外 —— 見下文詳述)。必須繼承自 object 。會像運算子過載一樣隱藏副作用。對子類來說會很難理解。

決定: 在那些你通常本來會用簡單、輕量的訪問/設定方法的程式碼中使用 Properties 來訪問和設定資料。只讀的屬性應該用@property描述符?來建立。

如果 Property 自身沒有被覆蓋,那 Properties 的繼承並非顯而易見。因此使用者必須確保訪問方法被間接呼叫,以便確保子類中被覆蓋了的方法會被 Property 呼叫(使用“模板方法(Template Method)”設計模式)。

Yes:

import math

class Square(object):
  """基類 square 有可寫的 'area' 屬性和只讀的 'perimeter' 屬性。

  可以這樣使用:
  >>> 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

  def _getArea(self):
    """計算 'area' 屬性的值"""
    return self.side**2

  def __getArea(self):
    """對 'area' 屬性的間接訪問器"""
    return self._getArea()

  def _setArea(self, area):
    """對 'area' 屬性的設定器"""
    self.side = math.sqrt(area)

  def __setArea(self, area):
    """對 'area' 屬性的間接設定器"""
    self._setArea(area)

  area = property(__getArea, __setArea,
                  doc="""Get or set the area of the square""")

  @property
  def perimeter(self):
    return self.side*4

== True/False 求值 

是什麼: 在布林型上下文下, Python 把一些特定的取值當作“false”處理。快速的“經驗法則”是所有的“空”值都會被認為是“false”,也即0None[]{}"" 在布林型上下文中都會被當作“false”。

優點: 使用 Python 的布林型條件更容易閱讀而且更不容易產生錯誤。在大多數情況下,這也是執行速度更快的選擇。

缺點: 對 C/C++ 開發者來說可能會看起來很奇怪。

決定: 在所有可能的情況下使用這種“隱含”的 false ,比如用if foo: 而不是if foo != []: 。這有幾條你應該時刻注意的限制條件:

  • 與具有唯一性的值比如 None 進行比較時總是應該使用 is 或者 is not。而且,留神在寫 if x: 而你的實際意思是 if x is not None: 的時候,比如,測試一個預設值是None的變數或引數是否被設定為其他值。這裡的“其他值”就有可能是在布林型上下文中被認為是 false 的值!
  • 對序列(strings、 lists、 tuples)來說,可以利用空序列就是 false 這一事實,也就是說 if not seq: 或者 if seq: 比 if len(seq): 或者 if not len(seq): 這種形式要更好。
  • 注意 '0' (也即 0 當作字串)會被求值為 true。

2.14  布林內建型別

是什麼: 從 Python 2.3 開始加入了布林型別,也即加入了兩個新的內建常量:True 和False

優點: 這使程式碼更容易閱讀而且與之前版本中使用整數作為布林型的做法向後相容。

缺點: 沒有。

決定: 使用布林型。

2.15  String 方法

是什麼: String 物件包括一些以前是string 模組中的函式的方法。

優點: 無需匯入string 模組,而且這些方法在標準 byte 字串和 unicode 字串上都能使用。

缺點: 沒有。

決定: 用吧。string 模組已經被廢棄了,現在更推薦使用 String 方法。

No:words = string.split(foo, ':')

Yes:words = foo.split(':')

2.16  靜態域

是什麼: 被巢狀的 Python 函式(nested Python function)可以引用定義在容器函式(enclosing function)中的變數,但無法對它們重新賦值。變數繫結依據靜態域(Lexical Scoping)決定,也就是說,基於靜態的程式文字(static program text)。程式碼塊中對一個名字的任意賦值都將導致 Python 把對這個名字的所有引用當作本地變數,即使是先呼叫後賦值。如果存在全域性變數宣告,那這個名字就會被當作是全域性變數。

以下是使用這一特性的例子:

def getAdder(summand1):
  """getAdder 返回把數字與一個給定數字相加的函式。"""
  def anAdder(summand2):
    return summand1 + summand2

  return anAdder

優點: 一般會得到更清晰、更優雅的程式碼。而且特別符合有經驗的 Lisp 和 Scheme (以及 Haskell、ML 等等)程式設計師的習慣。

缺點: 沒有。

決定: 可以用。

2.17  函式和方法修飾符

是什麼: 在 Python 2.4 版本增加了函式和方法的修飾符(又稱“@ notation”)。最常用的修飾符是@classmethod 和@staticmethod,用於把普通的方法轉化為類方法或靜態方法。然而,修飾符語法同樣允許使用者自定義修飾符。具體地,對函式myDecorator 來說:

class C(object):
  @myDecorator
  def aMethod(self):
    # method body ...

和如下程式碼是等價的:

class C(object):
  def aMethod(self):
    # method body ...
  aMethod = myDecorator(aMethod)

優點: 能夠優雅地對指定方法進行變形,而且這種變形避免了重複程式碼,強化了通用性(enforce invariants)等。

缺點: 修飾符能對函式的引數和返回值進行任意操作,會導致出乎意料之外的隱含行為。除此之外,修飾符會在匯入階段被執行。修飾符程式碼中的故障幾乎是不可能被修復的。

決定: 在有明顯好處時使用修飾符是明智的。修飾符應該和函式遵循同樣的匯入和命名準則。修飾符的__doc__ string 應該清晰地宣告該函式是一個修飾符,而且要為修飾符寫單元測試。

避免在修飾符資深引入外部依賴關係(比如,不要依賴檔案、 sockets 、資料庫連線等),因為在修飾符執行時(在匯入階段,也許源於pychecker 或其他工具)這些都有可能是無效的。在所有情況下都應(儘可能)保證使用有效引數呼叫修飾符時能成功執行。

修飾符是“頂級程式碼”(top level code)的一種特例 —— 參考主入口章節的更多討論。

2.18  執行緒

Google App Engine 不支援執行緒,所以在SoC framework 和 Melange Web 應用程式中也別用它。

2.19  高階特性

是什麼: Python 是一種極為靈活的語言,提供諸如 metaclass、 bytecode 訪問、即時編譯、動態繼承、 object reparenting、 import hacks、反射、修改系統內部結構等很多很炫的特性。

優點: 這些都是很強大的語言特性,能使你的程式碼更緊湊。

缺點: 在並非絕對必要的時候使用這些很“酷”的特性是很誘人的。裡面使用了這些並不常見的特性的程式碼會更難讀、更難懂、更難 debug。也許在剛開始的時候好像還沒這麼糟(對程式碼的原作者來說),但當你重新回到這些程式碼,就會覺得這比長點但簡單直接的程式碼要更難搞。

決定: 在 Melange 程式碼中避免使用這些特性。例外的情形在開發者郵件列表中討論。


3.  Python 編碼風格方面的準則

3.1  分號

不要用分號作為你的行結束符,也不要利用分號在同一行放兩個指令。

3.2  每行長度

一行最多可以有80個字元。

例外: 匯入模組的行可以超過80個字元再結束。

確保 Python 隱含的連線行(line joining)放在圓括號、方括號或大括號之間。如果需要的話,你可以在表示式兩頭放一堆額外的圓括號。

Yes:

fooBar(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')):

當表示文字的字串(literal string)一行放不下的時候,用圓括號把隱含的連線行括起來。

x = ('This will build a very long long '
     'long long long long long long string')

注意上例中連續行中元素的縮排,參考縮排章節的解釋。

3.3  圓括號

吝嗇地使用圓括號。在以下情況下別用:

  • 在 return 語句中。
  • 在條件判斷語句中。除非是用圓括號來暗示兩行是連在一起的(參見上一節)。
  • 在元組(tuple)周圍。除非因為語法而不得不加或是為了顯得更清晰。

但在下列情況下是可以用圓括號的:

  • 暗示兩行是連在一起的。
  • 用來括起長表示式中的子表示式(包括子表示式是條件判斷語句一部分的情形)。

實際上,在子表示式周圍用圓括號比單純依賴運算子優先順序要好。

Yes:

if foo:

while x:

if x and y:

if not x:

if (<3)and(not y):

return foo

for x, y in dict.items():

x, (y, z) = funcThatReturnsNestedTuples()

No:

if (x):

while (x):

if not(x):

if ((x<3)and(not y)):

return (foo)

for (x, y)indict.items():

(x, (y, z)) = funcThatReturnsNestedTuples()

3.4  縮排

注意和PEP8不同,這裡遵循作為本文起源的原始 Google Python 編碼風格指南。

用兩空格縮排程式碼塊。不要用 tab 或者混用 tab 和空格。如果要暗示兩行相連,要麼就把被包裝的元素縱向對其,就像“每行長度”章節中的例子那樣;要麼就用4個空格(不是兩個,這樣可以和後面緊跟著的巢狀程式碼塊區分開,避免混淆)作懸掛縮排(hanging indent),在這種情況下,首行不應該放任何引數。

Yes:

# 與起始定界符對齊:
foo = longFunctionName(var_one, var_two,
                       var_three, var_four)

# 4空格懸掛縮排,而且首行空著:
foo = longFunctionName(
    var_one, var_two, var_three,
    var_four)

No:

# 不應該在首行塞東西:
foo = longFunctionName(var_one, var_two,
    var_three, var_four)

# 不應該用兩空格的懸掛縮排:
foo = longFunctionName(
  var_one, var_two, var_three,
  var_four)

3.5  空行

在頂級定義(可以是函式或者類定義)之間加兩個空行。在方法定義之間以及“ class ”那一行與第一個方法之間加一個空行。在__doc__ string和它後面的程式碼之間加一個空行。在函式和方法內部你認為合適的地方加一個空行。

在檔案最後總是加一個空行,這可以避免很多 diff 工具生成“No newline at end of file”資訊。

3.6  空格

在圓括號、方括號、大括號裡面不要加空格。

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

在表示引數、列表、下標、分塊開始的圓括號/方括號前面不要加空格。

Yes:spam(1)

No:spam (1)

Yes:dict['key'] = list[index]

No:dict ['key'] = list [index]

在二元運算子兩邊各家一個空格,包括:賦值(=)、比較(==<>!=<><=>=innot inisis not)、以及布林運算子(andornot)。你肯定能判斷出是否應該在算術運算子周圍加空格,因為在二元運算子兩邊加空格的原則總是一致的。

Yes:x == 1

No:x<1

等號(“=”)用於指名引數或預設引數值時,兩邊不要加空格。

Yes:def Complex(real, imag=0.0): return Magic(r=real, i=imag)

No:def Complex(real, imag = 0.0): return Magic(r = real, i = imag)

3.7  Python 直譯器

模組開頭應該是一個“shebang”行,用於指定執行此程式的 Python 直譯器:

#!/usr/bin/python2.5

Google App Engine 要求使用 Python 2.5

3.8  註釋

Doc strings 

Python 有一種獨特的註釋風格稱為 __doc__ String。包、模組、類、函式的第一個語句如果是字串那麼就是一個__doc__ String。這種字串可以用物件的__doc__() 成員函式自動提取,而且會被用於pydoc。(試著在你的模組上執行pydoc 來看看它是怎麼工作的。)我們對__doc__ String 的約定是:用三個雙引號把字串括起來。__doc__String 應該這樣組織:首先是一個以句號結束的摘要行(實實在在的一行,不多於80個字元),然後接一個空行,再接__doc__ String 中的其他內容,並且這些文字的起始位置要和首行的第一個雙引號在同一列。下面幾節是關於__doc__ String 格式更進一步的原則

模組

每個檔案開頭都應該包含一個帶有版權資訊和許可宣告的塊註釋。

版權和許可宣告

#!/usr/bin/python2.5
#
# Copyright [current year] the Melange authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

#   http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

然後接著寫描述模組內容的__doc__ String,其中作者資訊應該緊跟著許可宣告。如果還給出了作者郵件地址,那新增到__authors__ 列表的整個作者字串應該就是一個與 RFC 2821 相容的電子郵件地址。

模組頭及作者資訊

"""用一行文字概述模組或指令碼,用句號結尾。

留一個空行。本 __doc__ string 的其他部分應該包括模組或指令碼的全面描述。作為可選項,還可以包括匯出的類和函式的簡要描述。

  ClassFoo: 一行概述。
  functionBar(): 一行概述。
"""


__authors__ = [
  # 請按照姓氏字母順序排列:
  '"John Smith" <johnsmith@example.com>',
  '"Joe Paranoid" <joeisgone@example.com>',  # 應提供電子郵件地址
]

如果新的貢獻者還沒新增到AUTHORS file中,那應該在該貢獻者第一次向版本控制工具提交程式碼時同時把他/她新增到這個檔案裡。

函式和方法 

如果不是用途非常明顯而且非常短的話,所有函式和方法都應該有 __doc__ string 。此外,所有外部能訪問的函式和方法,無論有多短、有多簡單,都應該有__doc__ string 。__doc__ string 應該包括函式能做什麼、輸入資料的具體描述(“Args:”)、輸出資料的具體描述(“Returns:”、“Raises:”、或者“Yields:”)。__doc__ string 應該能提供呼叫此函式相關的足夠資訊,而無需讓使用者看函式的實現程式碼。如果引數要求特定的資料型別或者設定了引數預設值,那__doc__ string 應該明確說明這兩點。“Raises:”部分應該列出此函式可能丟擲的所有異常。生成器函式的__doc__ string 應該用“Yields:”而不是Returns:

函式和方法的 __doc__ string 一般不應該描述實現細節,除非其中涉及非常複雜的演算法。在難以理解的程式碼中使用塊註釋或行內註釋會是更合適的做法。

def fetchRows(table, keys):
  """取出表中的多行內容。

  Args:
    table: 開啟的表。 Table 類的例項。
    keys: 字串序列,表示要從表中取出的行的鍵值。

  Returns:
    一個字典,對映指定鍵值與取出的表中對應行的資料:

    {'Serak': ('Rigel VII', 'Preparer'),
     'Zim': ('Irk', 'Invader'),
     'Lrrr': ('Omicron Persei 8', 'Emperor')}

    如果 keys 引數中的鍵值沒有出現在字典裡,就表示對應行在表中沒找到。

  Raises:
    IOError: 訪問 table.Table 物件時發生的錯誤。
  """

  pass
類應該在描述它的類定義下面放 __doc__ string 。如果你的類有公開屬性值,那應該在__doc__ string 的Attributes: 部分寫清楚。
class SampleClass(object):

  """這裡是類的概述。

  詳細的描述資訊……
  詳細的描述資訊……

  Attributes:
    likes_spam: 布林型,表示我們是否喜歡垃圾郵件。
    eggs: 整數,數我們下了多少蛋。
  """


  def __init__(self, likes_spam=False):
    """拿點什麼來初始化 SampleClass 。

    Args:
      likes_spam: 初始化指標,表示 SampleClass 例項是否喜歡垃圾郵件(預設是 False)。
    """

    self.likes_spam = likes_spam
    self.eggs = 0

  def publicMethod(self):
    """執行一些操作。"""
    pass
塊註釋及行內註釋 
加註釋的最後一個位置是在難以理解的程式碼裡面。如果你打算在下一次程式碼複查(code review)的時候解釋這是什麼意思,那你應該現在就把它寫成註釋。在開始進行操作之前,就應該給複雜的操作寫幾行註釋。對不直觀的程式碼則應該在行末寫註釋。
# 我們用帶權的字典檢索來查詢 i 在陣列中的位置。我們根據陣列中最大的數和
# 陣列的長度來推斷可能的位置,然後做二分法檢索找到準確的值。

if i & (i-1) ==0:        # 當且僅當 i 是 2 的冪時,值為 true

這些註釋應該和程式碼分開才更易讀。在塊註釋值錢應該加一個空行。一般行末註釋應該和程式碼之間至少空兩個格。如果連續幾行都有行末註釋(或者是在一個函式中),可以把它們縱向對齊,但這不是必須的。

另一方面,不要重複描述程式碼做的事。假設正在讀程式碼的人比你更懂 Python (儘管這不是你努力的方向)。On the other hand, never describe the code. Assume the person readingthe code knows Python (though not what you're trying to do) betterthan you do.

# *不好的註釋*:現在要遍歷陣列 b 並且確保任何時候 i 出現時,下一個元素都是 i+1
3.9  類

如果不從其他基類繼承,那就應該明確地從 object 基類繼承。這一條對巢狀類也適用。

No:

class SampleClass:
  pass

class OuterClass:
  class InnerClass:
    pass

Yes:

class SampleClass(object):
  pass

class OuterClass(object):
  class InnerClass(object):
    pass

class ChildClass(ParentClass):
  """已經從另一個類顯式繼承了。"""
  pass

從 object 繼承是為了讓類屬效能夠正常工作,這會避免我們一旦切換到 Python 3000 時,打破已經習慣了的特有風格。同時這也定義了一些特殊函式,來實現物件(object)的預設語義,包括:__new____init____delattr____getattribute____setattr____hash____repr__、和__str__

3.10  字串

應該用 運算子來格式化字串,即使所有的引數都是字串。不過你也可以在 + 和 之間做出你自己最明智的判斷。

No:

x = '%s%s' %(a, b)  # 這種情況下應該用 +
x = imperative + ', ' + expletive + '!'
x = 'name: ' + name + '; score: ' +str(n)

Yes:

x = a + b
x = '%s, %s!' % (imperative, expletive)
x = 'name: %s; score: %d' % (name, n)

應該避免在迴圈中用 + 或 += 來連續拼接字串。因為字串是不變型,這會毫無必要地建立很多臨時物件,從而二次方級別的運算量而不是線性運算時間。相反,應該把每個子串放到 list 裡面,然後在迴圈結束的時候用 ''.join() 拼接這個列表。

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

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)

對多行字串使用 """ 而不是 '''。但是注意,通常使用隱含的行連線(implicit line joining)會更清晰,因為多行字串不符合程式其他部分的縮排風格。

No:

print """這種風格非常噁心。
不要這麼用。
"""

Yes:

print ("這樣會好得多。\n"
           "所以應該這樣用。\n")
3.11  TODO style
在程式碼中使用 TODO 註釋是臨時的、短期的解決方案,或者說是足夠好但不夠完美的辦法。

TODO 應該包括全部大寫的字串 TODO ,緊接用圓括號括起來的你的使用者名稱: TODO(username) 。其中冒號是可選的。主要目的是希望有一種一致的TODO 格式,而且可以通過使用者名稱檢索。

# TODO(someuser): 這裡應該用 "*" 來做級聯操作。
# TODO(anotheruser) 用 relations 來修改這兒。

如果你的 TODO 是“在未來某個時間做某事”的形式,確保你要麼包括非常確定的日期(“Fix by November 2008”)或者非常特殊的事件(“在資料庫中的 Foo 實體都加上新的 fubar 屬性之後刪除這段程式碼。”)。

3.12  import 分組及順序

應該在不同的行中做 import :

Yes:

import sys
import os

No:

import sys,os

import 總是放在檔案開頭的,也即在所有模組註釋和 __doc__ string 的後面,在模組全域性變數及常量的前面。import 應該按照從最常用到最不常用的順序分組放置:

  • import 標準庫
  • import 第三方庫
  • import Google App Engine 相關庫
  • import Django 框架相關庫
  • import SoC framework 相關庫
  • import 基於 SoC 框架的模組
  • import 應用程式特有的內容

應該按照字母順序排序,但所有以 from ... 開頭的行都應該靠前,然後是一個空行,再然後是所有以import ... 開頭的行。以import ... 開頭的標準庫和第三方庫的 import 應該放在最前面,而且和其他分組隔開:

import a_standard
import b_standard
import a_third_party
import b_third_party

from a_soc import f
from a_soc import g

import a_soc
import b_soc

在 import/from 行中,語句應該按照字母順序排序:

from a import f
from a import g
from a.b import h
from a.d import e

import a.b
import a.b.c
import a.d.e
3.13  語句
一般一行只放一個語句。但你可以把測試和測試結果放在一行裡面,只要這樣做合適。具體來說,你不能這樣寫 try/except ,因為try 和except 不適合這樣,你只可以對不帶else 的if 這麼幹。

Yes:

if foo: fuBar(foo)

No:

if foo: fuBar(foo)
else:   fuBaz(foo)

try:               fuBar(foo)
except ValueError: fuBaz(foo)

try:
  fuBar(foo)
except ValueError: fuBaz(foo)
3.14  訪問控制
如果存取器函式很簡單,那你應該用公開的變數來代替它,以避免 Python 中函式呼叫的額外消耗。在更多功能被加進來時,你可以用 Property 特性來保持語法一致性。

另一方面,如果訪問更復雜,或者訪問變數的成本較為顯著,那你應該用函式呼叫(遵循命名準則),比如getFoo() 或者setFoo()。如果過去的行為允許通過 Property 訪問,那就別把新的存取器函式繫結到 Property 上。所有仍然企圖用舊方法訪問變數的程式碼應該是非常顯眼的,這樣呼叫者會被提醒這種改變是比較複雜的。

3.15  命名

要避免的命名方式

  • 使用單個字元命名,除非是計數器或迭代器。
  • 在任何包或模組的名字裡面使用減號。
  • __double_leading_and_trailing_underscore__ 在變數開頭和結尾都使用兩個下劃線(在 Python 內部有特殊含義)。

命名約定

注意這裡某些命名約定和PEP8不一樣,而是遵循“Google Python 編碼風格指南”的原始版本,也即本編碼風格指南的起源。

  • “Internal”表示模組內部或類中的保護域(protected)和私有域。
  • 變數名開頭加一個下劃線(_)能對保護模組中的變數及函式提供一些支援(不會被 import * from 匯入)。
  • 在例項的變數和方法開頭加兩個下劃線(__)能有效地幫助把該變數或方法變成類的私有內容(using name mangling)。
  • 把模組中相關的類和頂級函式放在一起。不像 Java ,這裡無需要求自己在每個模組中只放一個類。但要確保放在同一模組中的類和頂級函式是高內聚的
  • 對類名使用駝峰式(形如 CapWords),而對模組名使用下劃線分隔的小寫字母(lower_with_under.py)。

命名樣例

類別公開的內部的
Packageslower_with_under 
Moduleslower_with_under_lower_with_under
ClassesCapWords_CapWords
ExceptionsCapWords 
FunctionsfirstLowerCapWords()_firstLowerCapWords()
Global/Class ConstantsCAPS_WITH_UNDER_CAPS_WITH_UNDER
Global/Class Variableslower_with_under_lower_with_under
Instance Variableslower_with_under_lower_with_under (protected) or__lower_with_under(private)
Method Names *firstLowerCapWords()_firstLowerCapWords() (protected) or__firstLowerCapWords()(private)
Function/Method Parameterslower_with_under 
Local Variableslower_with_under 

* 考慮只在首選項設定中使用對公開屬性的直接訪問來作為 getters 和 setters,因為函式呼叫在 Python 中是比較昂貴的,而property 之後可以用來把屬性訪問轉化為函式呼叫而無需改變訪問語法。

3.16 程式入口

所有的 Python 原始碼檔案都應該是可匯入的。在 Python 中,pycheckerpydoc、 以及單元測試都要求模組是可匯入的。你的程式碼應該總是在執行你的主程式之前檢查if __name__ == '__main__': ,這樣就不會在模組被匯入時執行主程式。大寫main() 是故意要和命名約定的其他部分不一致,也就是說建議寫成 Main() 。

if __name__ == '__main__':
  # 引數解析
  main()

在模組被匯入時,所有頂級縮排程式碼會被執行。注意不要呼叫函式、建立物件、或者做其他在檔案被 pycheck 或 pydoc 的時候不應該做的操作。

3.17  總結

要一致

如果你在編寫程式碼,花幾分鐘看看你周圍的程式碼並且弄清它的風格。如果在 if 語句周圍使用了空格,那你也應該這樣做。如果註釋是用星號組成的小盒子圍起來的,那你同樣也應該這樣寫。

編碼風格原則的關鍵在於有一個程式設計的通用詞彙表,這樣人們可以人民可以集中到你要說什麼而不是你要怎麼說。我們在這裡提供全域性的編碼風格規則以便人們知道這些詞彙,但區域性風格也重要。如果你加入檔案中的程式碼看起來和裡面已經有的程式碼截然不同,那在讀者讀它的時候就會被破壞節奏。儘量避免這樣。

[Python 編碼風格指南中譯版(Google SOC)]

from: http://blog.csdn.net/pipisorry/article/details/26840461

ref: 《The Poetry of Function Naming》函式名的藝術

Google Python Style Guide


相關文章