1. 程式人生 > >Python程式碼風格指南&程式設計規範

Python程式碼風格指南&程式設計規範

1 前言

        紛吾既有此內美兮,又重之以修能。 ---- 屈原《離騷》

 1.1 編寫目的

        本文用於指導我司使用python語言進行開發的人員。

1.2 範圍

        測試人員、開發人員以及其他python語言使用者。

2 簡介

        很多語言都有自己獨有的編碼風格,python以簡潔優美著稱,當然也不能例外。如果和本文規則發生任何衝突,請優先與專案級別的程式碼風格保持一致。程式碼風格一致性當然重要,想象一下空姐的制服誘惑,是不是賞心悅目呢?但也要有自己的主觀判斷,不墨守陳規,愚蠢的追求程式碼一致性,例如有以下的場景:

1、遵循此風格寫的一小片程式碼看起來和專案內其他程式碼格格不入,看到就想摳出來餵豬,人人見而曰日之。

2、遵循此風格後和其他 python 版本不相容,甚至出現錯誤,這就尷尬了。

3、遵循此風格中的某些條目使程式碼更不易讀,簡單說就是醜。

3 程式碼佈局

3.1 縮排

        一個縮排級別四個空格。

        1. 連續行使用兩種方式使封裝元素成為一行:括號內垂直隱式連線 & 懸掛式縮排。 使用懸掛式縮排應該注意第一行不應該有引數,連續行要使用進一步的縮排來區分。

是:

# 括號內隱式連線,垂直對齊

foo = long_function_name(var_one, var_two,

                         var_three, var_four)

# 懸掛縮排,進一步縮排區分其他語句def long_function_name(

        var_one, var_two, var_three,

        var_four):

    print(var_one)

# 懸掛縮排,一般是四個空格,但非必須

foo = long_function_name(

    var_one, var_two,

    var_three, var_four)



否:

# 括號內隱式連線,沒有垂直對齊時,第一行的引數被禁止

foo = long_function_name(var_one, var_two,

    var_three, var_four)

# 懸掛縮排,需要進一步的縮排區分其他行def long_function_name(

    var_one, var_two, var_three,

    var_four):

    print(var_one)

 

        2.當 if 語句過長時,可選的處理方式,但不限於此:

# 不使用額外縮排if (this_is_one_thing and

    that_is_another_thing):

    do_something()

# 增加註釋區分,支援語法高亮if (this_is_one_thing and

    that_is_another_thing):

    # Since both conditions are true, we can frobnicate.

    do_something()

    # 條件連續行額外縮排if (this_is_one_thing

        and that_is_another_thing):

    do_something()

        3.當閉環括號內元素跨行時,可以採用以下方式。

my_list = [
    1, 2, 3,
    4, 5, 6,
    ]
result = some_function_that_takes_arguments(
    'a', 'b', 'c',
    'd', 'e', 'f',
    )

或者

my_list = [
    1, 2, 3,
    4, 5, 6,
]
result = some_function_that_takes_arguments(
    'a', 'b', 'c',
    'd', 'e', 'f',
)

3.2 空格和tab

        Python 3 不允許 tab 和 space 混用,同時混用了 tab 和 space 的 Python 2 程式碼應該被轉換為僅使用 space。

3.3 語句中的空格

        在以下場景避免不必要的空格:

是: spam(ham[1], {eggs: 2})
否: spam( ham[ 1 ], { eggs: 2 } )

是: foo = (0,)
否: bar = (0, )

是: if x == 4: print x, y; x, y = y, x
否: if x == 4 : print x , y ; x , y = y , x

是:
    ham[1:9], ham[1:9:3], ham[:9:3], ham[1::3], ham[1:9:]
    ham[lower:upper], ham[lower:upper:], ham[lower::step]
    ham[lower+offset : upper+offset]
    ham[: upper_fn(x) : step_fn(x)], ham[:: step_fn(x)]
    ham[lower + offset : upper + offset]
否:
    ham[lower + offset:upper + offset]
    ham[1: 9], ham[1 :9], ham[1:9 :3]
    ham[lower::upper]
    ham[:upper]
    
是: spam(1)
否: spam (1)

是: dct['key'] = lst[index]
否: dct ['key'] = lst [index]

是:
    x = 1
    y = 2
    long_variable = 3
  
否:
    x             = 1
    y             = 2
    long_variable = 3

        在任何地方避免使用尾隨空格

        在二元運算子周圍使用空格:

是:
    i = i + 1
    submitted += 1
    x = x*2 - 1
    hypot2 = x*x + y*y
    c = (a+b) * (a-b)
    
否:
    i=i+1
    submitted +=1
    x = x * 2 - 1
    hypot2 = x * x + y * y
    c = (a + b) * (a - b)

        表示關鍵字引數或預設引數值時,不要使用空格:

是:
    def complex(real, imag=0.0):
        return magic(r=real, i=imag)
否:
    def complex(real, imag = 0.0):
        return magic(r = real, i = imag)

        函式註解的場景:

是:
    def munge(input: AnyStr): ...
    def munge() -> AnyStr: ...
否:
    def munge(input:AnyStr): ...
    def munge()->PosInt: ...

        當引數註釋和預設值共存時:

是:
    def munge(sep: AnyStr = None): ...
    def munge(input: AnyStr, sep: AnyStr = None, limit=1000): ...
否:
    def munge(input: AnyStr=None): ...
    def munge(input: AnyStr, limit = 1000): ...

        同行多語句不建議使用:

是:
    if foo == 'blah':
        do_blah_thing()
    do_one()
    do_two()
    do_three()
否:
    if foo == 'blah': do_blah_thing()
    do_one(); do_two(); do_three()

        下面這種醜就不多說了:

否:
    if foo == 'blah': do_blah_thing()
    for x in lst: total += x
    while t < 10: t = delay()
否:
    if foo == 'blah': do_blah_thing()
    else: do_non_blah_thing()

    try: something()
    finally: cleanup()

    do_one(); do_two(); do_three(long, argument,
                                 list, like, this)

    if foo == 'blah': one(); two(); three()

3.4 程式碼行最大長度

        將所有行限制為最多79個字元。對於具有較少結構限制(文件字串或註釋)的長文字塊,行長度應限制為72個字元。當然了,不要問為啥非得是 7972。因為要兼顧非洲大陸人民的生活。程式碼是全世界的。反斜槓有時可能仍然要用。 例如,又多又長的 with - 語句不能使用隱式連線,這時反斜槓是可以接受的:

with open('/path/to/some/file/you/want/to/read') as file_1, \
     open('/path/to/some/file/being/written', 'w') as file_2:
    file_2.write(file_1.read())

assert 語句也是如此。

3.5 運算子前還是後斷行?

        演算法和程式設計技術先驅,計算機排版系統 TEX 和 METAFONT 的發明者 Donald Knuth,推薦使用以下形式:

# 是: easy to match operators with operands
income = (gross_wages
          + taxable_interest
          + (dividends - qualified_dividends)
          - ira_deduction
          - student_loan_interest)

       只要保持本地一致性,在二元運算子之前和之後斷開都是允許的,但是新的 Python 程式碼推薦使用 Knuth 形式。

3.6 空行

        頂層函式和類定義間使用兩個空行。

        類內方法定義間使用一個空行。

       不同函式組之間使用兩個空行隔離。

       總之,空行的作用就是隔離不同函式類等,使層次分明。

3.7 原始檔編碼

       Python 2 預設ASCII,Python 3 預設UTF-8。使用 ASCII 的 Python 2 原始檔或使用 UTF-8 的 Python 3 原始檔不應該有編碼宣告。原始檔最好只使用 ASCII 字元,即使是蹩腳的 Chinglish 亦可,家和萬事興。

3.8 模組匯入

是:
    from subprocess import Popen, PIPE
    import os
    import sys
否:
    import sys, os

        模組匯入總是位於檔案頂部,在模組註釋和文件字串之後,模組全域性變數和常量之前。

匯入應該按照以下順序分組,不同組間用空行隔離。

  1. 標準庫 imports  
  2. 相關第三方 imports
  3. 本地特定應用/庫 imports  

       推薦使用絕對匯入,標準庫程式碼應總是使用絕對匯入。

import mypkg.sibling
from mypkg import sibling
from mypkg.sibling import example

        在包結構比較複雜時,可以使用相對匯入。

from . import sibling
from .sibling import example

        在 Python 3 中,相對匯入已經被刪除,禁止使用。類匯入:

from myclass import MyClass
from foo.bar.yourclass import YourClass

        如果這種方式導致了本地命名衝突,可以使用以下方式:

import myclass
import foo.bar.yourclass

       然後使用 myclass.MyClass 和 foo.bar.yourclass.YourClass。

請不要使用以下方式:

from <module> import *

3.9 模組級別 dunder 名稱

         模組級別 “dunders”(即具有兩個前導和兩個字尾下劃線的名稱),例如 __all__,__author__,__version__ 等應放在模組 docstring 之後,但在任何 import 語句之前,但是除了 __future__ 匯入。 Python 強制 future-imports 必須在除了 docstrings 之外的任何其他程式碼之前出現在模組中。  

例如:

"""This is the example module.This module does stuff."""
from __future__ import barry_as_FLUFL
__all__ = ['a', 'b', 'c']__version__ = '0.1'
__author__ = 'Cardinal Biggles'
import os
import sys

4 字串引號,逗號

4.1 字串引號

在 Python 中,單引號和雙引號是等價的,只需要堅持使用一種並保持一致即可。

在雙引號中使用單引號,單引號中使用雙引號。三引號中使用雙引號。

4.2 逗號

單元素元組強制使用逗號:

是:

    FILES = ('setup.cfg',)

OK, but confusing:

    FILES = 'setup.cfg',

當使用版本控制系統時,一組希望後續擴充套件的值/引數/改善的條目使用以下形式:

是:

    FILES = [

        'setup.cfg',

        'tox.ini',

        ]

    initialize(FILES,

               error=True,

               )

否:

    FILES = ['setup.cfg', 'tox.ini',]

    initialize(FILES, error=True,)

 

5 註釋

       糟糕的註釋不如沒有註釋,最好要用 English 註釋!

5.1 塊註釋

       同等級別的一塊程式碼的註釋,塊註釋內每行註釋以 # 開頭,內部註釋段落之間使用以 # 開頭的空行註釋隔開。

5.2 註釋

       行註釋和程式碼宣告間至少間隔兩個空格,不要使用無聊的行註釋,例如:

Don't do this:

x = x + 1                 # Increment x

But sometimes, this is useful:

x = x + 1                 # Compensate for border

5.3 文件字串

        為所有公共模組,函式,類和方法編寫文件字串。 對於非公共方法,文字字串不是必需的,但應該有一個描述該方法的註釋。例如:

"""Return a foobangOptional plotz says to frobnicate the bizbaz first."""

"""only one single docstring line"""

       注意當註釋為多行時,最終的 """ 單獨起一行。

6 命名約定

        對使用者可見的公共 API 部分的命名應該遵從反應如何使用而不是怎麼實現

6.1 描述性: 命名風格

        以下命名風格通常區分彼此使用:

  • b (單個小寫字母)
  • B (單個大寫字母)
  • lowercase(小寫)
  • lower_case_with_underscores(帶下劃線的小寫)
  • UPPERCASE(大寫)
  • UPPER_CASE_WITH_UNDERSCORES(帶下劃線的大寫)
  • CapitalizedWords(駝峰式,蒙古包式 whatever.)

Note: 使用駝峰式時,縮寫全部大寫,例如:HTTPServerError 好於 HttpServerError

  • mixedCase (烏鬼頭)
  • Capitalized_Words_With_Underscores (醜!不解釋!)
  • _single_leading_underscore : 弱地 "內部使用" 指示器. 例如,from M import * 不會匯入下劃線開頭的物件
  • single_trailing_underscore_ : 用來避免和 python 關鍵字衝突,例如:
Tkinter.Toplevel(master, class_='ClassName')
  • __double_leading_underscore : 當對類屬性命名時,呼叫名改變 (在 FooBar 類內,__boo 變成了 _FooBar__boo;後面有介紹)
  • __double_leading_and_trailing_underscore__ : "魔幻的" 物件或屬性,只生存於使用者控制的名稱空間。例如, __init__ ,__import__ 或 __file__ 。千萬不要臆造這種命名; only use them as documented.

6.2 規定性: 命名習慣

6.2.1 避免的命名

        一定不要使用 l(唉歐兒) O(偶) I(艾) 作為單字元變數命名,在某些字型中,這些字母和數字10無法區分。

6.2.2包和模組名

        模組應該使用簡短並且全小寫的命名,下劃線也可以使用以提升可讀性。

        Python 包也應該使用簡短的全小寫名稱,儘管不鼓勵使用下劃線。

        當 C/C++ 編寫的擴充套件模組伴隨一個提供更高級別介面的 python 模組時,C/C++ 模組命名應該以下劃線開頭(例如,_socket)。

6.2.3 類名

        類名通常使用駝峰式命名習慣

        在類的介面有文件說明,並且主要用於 callable 的情況下,類都是 callable 的,call 一個類將返回一個新的類例項,例如 instance = Class()。如果類實現了 __call__() 函式,那麼類例項也將是 callable 的,類的命名也可以使用函式命名習慣。對於 builtin 函式的命名習慣,可以通過 dir(__builtins__) 檢視系統函式命名樣例。注意區分普通命名,異常名命名和 builtin 常量。

6.2.4 型別變數名

        相對於短名稱如:T,AnyStr,Num,型別變數使用駝峰式命名習慣較好。另外建議在變數名前新增 _co 或 _contra 字首響應的宣告 covariant 或 contravariant 行為。例如:

from typing import TypeVar

VT_co = TypeVar('VT_co', covariant=True)KT_contra = TypeVar('KT_contra', contravariant=True)

6.2.5 異常名

        異常應該是類,所以可以使用類命名習慣,但是,如果異常是個錯誤類,一般加上 "Error" 字尾。

6.2.6 全域性變數名

        我們假設這些全域性變數只在一個模組內使用,這樣的話和函式的命名習慣是一樣的。設計為通過 from M import * 匯入的類應該使用 __all__ 機制避免匯出全域性變數,或者可以使用老式的習慣,給這些全域性變數名加上下劃線作為字首(表示這是非公有變數)。

6.2.7 函式名

        例如,func or func_write_to_file 為了向後相容性,也可以使用 mixedCase 式命名風格。

6.2.8 函式和方法引數

       例項方法第一個入參一定要是 self。

       類方法第一個入參一定要是 cls。

       如果函式入參名和保留關鍵字衝突,則字尾下劃線好過縮寫或者糟糕的拼寫。例如,class_ 好過 clss。

6.2.9 方法名和例項變數

        使用函式命名風格即可。如果希望是私有方法或例項變數,則字首下劃線。為避免和子類的命名衝突,請使用雙下劃線字首命名。如果類 Foo 有一個屬性變數 __a,那麼通過 Foo.__a 是不能被訪問的。當然,固執的使用者仍然可以通過 Foo._Foo__a 訪問),一般來說,雙下劃線字首只是在避免子類屬性命名衝突的場景下使用。

6.2.10 常量

        常量一般定義在模組級別。命名風格如:MAX_OVERFLOW 或 TOTAL 。

6.2.11 繼承設計

        經常去思考類方法和例項變數(屬性)應該是公有的還是非公有的(嚴格意義上,python 沒有私有變數)。如果不確定,那就設定成非公有的。另一類屬性類別是子類 API 的一部分,(在其他語言中稱"protected")。有些類天生就是被設計為用來繼承的,當設計這種類時,注意哪些屬性是公有的,哪些是子類 API 的一部分,哪些是隻在基類中使用的。

來自仁慈的python之父的指導:

  1. 公有例項變數不應該有字首下劃線。
  2. 公有例項變數和保留關鍵字衝突時,變數名加字首下劃線避免,這比使用縮寫和其他糟糕的拼寫要好(除了 'cls',當一個變數或入參確定是一個類,特別是作為類方法的第一個入參時,'cls' 更惹人喜愛)。
  3. 對於簡單的公有資料屬性,不要使用複雜的存取函式,直接暴露屬性名。
  4. 如果設計繼承基類時,不希望子類訪問的屬性加雙下劃線字首。

6.2.12 公共和內部介面

        文件說明的介面一般認為是公共介面,除非文件明確宣告為臨時或內部介面(為了相容性等其他原因),所有非文件說明的介面一般為內部介面。模組應該使用 __all__ 屬性明確宣告公共 API 名,如果 __all__ 為空,則表明模組沒有公共 API。儘管使用了 __all__ 屬性,內部介面(packages, modules, classes, functions, attributes or other names)仍然需要使用字首下劃線。如果包含的任何一個名稱空間(package, module or class)是內部的,那麼這個介面也被認為是內部介面。匯入名應該總是被視為實現細節。其他匯入模組一定不能依賴對此匯入名的間接訪問,除非它們是包含模組 API 的顯式文件說明的部分,例如os.path 或者一個 package 向子模組暴露函式的 __init__ 模組。

7 編碼建議

  • 程式碼不應該以一種不利於其他 python 實現(PyPy, Jython, IronPython, Cython, Psyco 諸如此類)的方式編寫。 例如:不要使用 a += b 或 a = a + b 來實現就地字串連線,在庫的效能敏感部分,應該使用 ''.join() 的形式,這就能保證在不同的 python 實現中,連線動作可以線上性時間內完成。
  • 和例如 None 這類 singleton 的比較,應該使用 is 或 is not 而不是 ==。另外,小心使用 if x 如果你的本意是 if x is not None,如果 x 是個布林變數值 false,那可就完蛋了。
  • 儘管功能相同,從可讀性上考慮:
是:

    if foo is not None:

否:

    if not foo is None:
  • 當使用 rich comparisons 實現排序操作時,最好是實現所有六種操作(__eq__,__ne__, __lt__, __le__, __gt__, __ge__)而不要依賴其他的程式碼去單獨實現某一類比較。為了減少勞動,functools.total_ordering() decorator 提供了一個生成缺失比較方法的工具。
  • 使用 def 語句而不要使用賦值語句去直接繫結一個 lambda 表示式到識別符號上:
是:

    def f(x): return 2*x

否:

    f = lambda x: 2*x
  • 賦值語句的使用消除了 lambda 表示式相對於顯式 def 語句的唯一好處,那就是它能夠嵌入到一個更大的表示式裡面。
  • 捕獲的異常要說明 "錯誤出在哪裡了 ?" 而不是僅僅說明 "哎呀!出問題了!"。
  • 正確使用異常連結。在 Python 3 中,應該使用 "raise X from Y" 來表示顯式替換並且不會丟失原始追溯。
  • 當有意替換一個內部異常(Python 2: "raise X", Python 3.3+: raise X from Non)時,請確保將相關的詳細資訊轉移到新的異常(例如,將 KeyError 轉換為 AttributeError 時保留屬性名稱,或將原始異常的文字嵌入到新的異常訊息中)。
  • 當在 Python 2 中丟擲異常時,使用 raise ValueError('message') 而不是老式的 raise ValueError, 'message',後者已經在 Python 3 中廢棄。由於使用了括號,可以避免行連續符的使用。
  • 當捕獲異常時,儘可能提及具體的異常而不是使用一個赤裸裸的 except 子句。一個裸露的 except: 子句將捕獲 SystemExit 和 KeyboardInterrupt 異常,這樣的話就難於使用 control-c 中斷程式,並可能掩蓋其他問題。如果想要捕獲標誌程式錯誤的所有異常的話,用 except Exception:(裸露的 except 子句等同於 except BaseException:):
try:

    import platform_specific_moduleexcept ImportError:

    platform_specific_module = None

        一個很好的經驗法則是將裸露的 except 子句僅用於以下兩種情況:

1、If the exception handler will be printing out or logging the traceback; at least the user will be aware that an error has occurred.

2、If the code needs to do some cleanup work, but then lets the exception propagate upwards with raise . try...finally can be a better way to handle this case.

  • 當對捕獲的異常重新命名時,使用 2.6 版本引入的語法:
try:

    process_data()except Exception as exc:

    raise DataProcessingFailedError(str(exc))
  • 當捕獲作業系統錯誤時,相對於內建的 errno 值,最好是使用 Python 3.3 中介紹的顯式異常層次結構。
  • 對於所有的 try/except 子句,將 try 子句限制為必需的絕對最小程式碼量避免隱藏 bug:
是:

    try:

        value = collection[key]

    except KeyError:

        return key_not_found(key)

    else:

        return handle_value(value)


否:

    try:

        # Too broad!

        return handle_value(collection[key])

    except KeyError:

        # Will also catch KeyError raised by handle_value()

        return key_not_found(key)
  • 特定程式碼塊的本地資源使用 with 語句確保使用後立即釋放,不能自動釋放的使用 try/finally 也可以。
  • 除了申請和釋放資源,任何時候都應該使用單獨的函式和方法呼叫 Context managers,例如:
是:

    with conn.begin_transaction():

        do_stuff_in_transaction(conn)

否:

    with conn:

        do_stuff_in_transaction(conn)
  • 函式返回語句要一致。在一個函式內的所有返回語句要麼都返回一個表示式,要麼都不返回。如果任何一個返回語句返回了表示式,那麼其他任何沒有返回值的語句應該明確宣告為 return None。在函式結束部分必須出現返回語句:
是:

    def foo(x):

        if x >= 0:

            return math.sqrt(x)

        else:

            return None

    def bar(x):

        if x < 0:

            return None

        return math.sqrt(x)

否:

    def foo(x):

        if x >= 0:

            return math.sqrt(x)

    def bar(x):

        if x < 0:

            return

        return math.sqrt(x)
  • 相對於 string 模組,使用 string 方法要快的多並且與 unicode strings 共享相同的 API。當然了,除了需要考慮 2.0 版本之前 python 程式碼向後相容性的情況。
  • 使用 ''.startswith() 和 ''.endswith() 而不是字串切片來檢查字首或字尾,例如:
是: if foo.startswith('bar'):

否: if foo[:3] == 'bar':
  • 物件型別比較應該使用isinstance() 而不是直接比較:
是: if isinstance(obj, int):

否: if type(obj) is type(1):
  • 當檢查一個物件是否為字串時,一定要注意這個物件也可能是 unicode 字串!在Python 2 中,string 和 unicode 擁有一個公共基類 basestring,因此可以這麼的:
if isinstance(obj, basestring):

        在 Python 3 中,unicode 和 basestring 已然不復存在(there's only str),並且 bytes object 也不再視為一種 string 了,而是一個整形序列。

  • 對於序列(字串,列表,元組)的判空操作:
是:
    if not seq:

    if seq:

否:
    if len(seq):

    if not len(seq):
  • 不要使用尾隨空格。
  • 不要使用 == 驗證布林值為 Ture 或 False:
是:

      if greeting:

否:

      if greeting == True:

也非:

      if greeting is True:

8 結束語

        本文主要參考的是PEP8 Python編碼規範和來源於網際網路上圈子內的的優秀實踐,吸納形成為我司的python編碼規範文件,計劃將這套規範用於指導我司python程式設計者進行作業,希望大家在日常的編碼中都能遵守,拿它當作鏡子,時常照照,對提升自身技能也大有裨益。當然,對本規範有任何不盡人意的地方都可以聯絡我進行修正完善。