[Python] PEP8 風格

程式語言:python
Package:pep8
官方文件

簡介:程式碼的風格指南

檢查方法

# 常用
pep8 --first optparse.py
# 顯示程式碼
pep8 --show-source --show-pep8 testsuite/E40.py
# 統計錯誤
pep8 --statistics -qq Python-2.5/Lib

忽略的條件

  • 採用時會讓代碼更難閱讀,甚至對於習慣閱讀 PEP8 的人也是如此。
  • 需要和周圍的代碼保持一致性,但這些代碼違反了指南中的風格
    —— 儘管這可能也是一個收拾別人爛攤子的機會
  • 若是有問題的某段代碼早於引入指南的時間,那麼沒有必要去修改這段代碼。 
  • 代碼需要和更舊版本 Python 保持兼容,而舊版本 Python 不支持所推薦的風格

代碼排版 (Code lay-out)

縮排 (Indentation)
請使用四個空格當作縮排
兩種縮排方式
  • Python 隱式續行,即圓括號、方括號和花括號換行不影響,而垂直對齊之
  • 懸掛縮排 (hanging indent)
    • 第一行不應該包括參數
    • 續行中需要再縮排一級以便清楚表示
正確方式
# 同左括號對齊
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 空格
# 懸掛縮排可以不採用 4 空格的縮排方法
foo = long_function_name(
  var_one, var_two,
  var_three, var_four)

多行 if 語句
PEP8 未明確規定
以下幾種方法皆可行的,但不僅僅只限於這幾種方法
# 不採用額外縮排
if (this_is_one_thing and
    that_is_another_thing):
    do_something()

# 增加一行註解,在編輯器中顯示時能有所區分
if (this_is_one_thing and
    that_is_another_thing):
    # 註解隔開
    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',
)

每行最大長度 (Maximum Line Length)
  • 限制 79 個字符長度
  • 文檔字符串 (docstring) 或 註解 則限制在 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())

邏輯運算符號位置
正確方式
# 放在前面
income = (gross_wages
          + taxable_interest
          + (dividends - qualified_dividends)
          - ira_deduction
          - student_loan_interest)

錯誤方式
# 放在後面
income = (gross_wages +
          taxable_interest +
          (dividends - qualified_dividends) -
          ira_deduction -
          student_loan_interest)

空行 (Blank line)
  • 2 個空行
    • 分隔 top function 和 class
  • 1 個空行
    • 分隔 class 的 method
  • 需用在刀口上
    • 分隔不同的 function set
    • 分割不同的邏輯塊
def function1():
    pass
    

def function2():
    pass
    

class class1():
    def mthod1():
        pass
        
    def mthod2():
        pass

Imports

分行寫
# 分開寫
import os
import sys
# 不要像下面一樣寫在一行
import sys, os
這樣寫也是可以的
from subprocess import Popen, PIPE

位置
  • 代碼文件的開頭
  • module 註解和文檔字符串之後
  • 全局變數 (globals) 和 常數 (constants) 宣告之前
順序
用空行隔開
  • 標準庫 imports
  • 相關第三方 imports
  • 本地應用/庫的特定 imports

absolute imports
import mypkg.sibling
from mypkg import sibling
from mypkg.sibling import example

明確的相對 imports
當處理複雜的 package layouts 時
from . import sibling
from .sibling import example

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

命名衝突
直接 import 模塊
import myclass
import foo.bar.yourclass

避免使用 from <module> import *
可能會造成互相覆蓋的情況

Module Level 雙底線變數 (dunder)

  • 像是 __all__ , __author__ , __version__ 
    • module docstring 之後
    • import 之前
  • __future__
    • 任何 code 之前
    • 除了 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

字符串引用 (String Quotes)

  • String 宣告
    • 不混用單引號 或 雙引號
    • 單一規則堅持使用
    • 三引號只使用雙引號(即是 """ 而不是 ''' )

表達式和語句中的空格
(Whitespace in Expressions and Statements)

避免使用過多的空白
方括號,圓括號和花括號之後
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

slice 操作
冒號和二元運算符是一樣的
  • 左右兩邊保留相同數量的空格
  • 參數被省略時,應該也忽略空格
正確方式
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

運算符
  • 不同優先級的運算符
    • 優先級較低的運算符增加空白
    • 不超過 1 個空格
    • 兩側的空白數量一樣
正確方式
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: ...

禁止多行語句寫在一行
正確方式
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()

句尾逗號 (When to use trailing commas )

Tuple
正確方式
    FILES = ('setup.cfg',)

錯誤方式
    FILES = 'setup.cfg',

不同行的 list/tuple 元素
可加入逗號,但同一行的情況下只是多餘的
正確方式
FILES = [
    'setup.cfg',
    'tox.ini',
    ]
initialize(FILES,
           error=True,
           )

錯誤方式
FILES = ['setup.cfg', 'tox.ini',]
initialize(FILES, error=True,)

註解 (Comments)

  • 和代碼矛盾的註解還不如沒有
  • 當代碼有改動時,優先更改註解使其保持最新
  • 註解應該是完整的多個句子
  • 註解是一個短語或一個句子,其首字母應該大寫
    除非是小寫字母開頭的 identifier(永遠不要更改 identifier 的大小寫)
  • 註解很短,結束的句號可以被忽略。
  • 塊註解通常由一段或幾段完整的句子組成,每個句子都應該以句號結束
  • 你應該在句尾的句號後再加上 2 個空格
  • 來自非英語國家的 Python 程序員們,請使用英文來寫註解
    除非你 120% 確定你的代碼永遠不會被不懂你所用語言的人閱讀到
塊註解(Block Comments)
  • 寫在對應代碼之前
  • 和對應代碼有同樣的縮排
  • 以 # 和一個空格開頭(除非該文本是在註解內縮排對齊的)
  • 空行用只含有單個 # 的一行隔開
# 這是一個註解
#
# 這是第二個註解

行內註解(Inline Comments)
行內註解是和代碼語句寫在一行內的註解
  • 儘量少用
  • 和代碼語句之間有 2 個空格的間隔
  • 以 # 和一個空格開始
非必要
x = x + 1                 # Increment x
有意義
x = x + 1                 # Compensate for border

文檔字符串 (Documentation Strings)

  • 所有的 public module,function,class 和 method 都應有 doc strings
  • 非公共方法,文檔字符串不是必要的,但應留有註解說明功能
    • 該註解應當出現在 def 的下一行
  • 以單行 """ 結尾,不能有其他字符
  • 只有單行的, """ 應寫在同一行
"""Return a foobang

Optional plotz says to frobnicate the bizbaz first.
"""

特別命名規範

  • _single_leading_underscore
    • 單個下劃線開頭表示「內部使用」的弱標誌
    • E.g. "from M import *" 不會 import 下劃線開頭的對象
  • single_trailing_underscore_
    • 單個下劃線結尾用來避免和 Python 關鍵詞產生衝突
      例如: Tkinter.Toplevel(master, class_='ClassName') 
  • __double_leading_underscore
    • 雙下劃線開頭的命名,class 屬性將觸發命名修飾,被重新命名之
      (在FooBar類中,"__boo" 命名會被修飾成 "_FooBar__boo")
  • __double_leading_and_trailing_underscore__
    • 雙下劃線開頭和結尾的命名風格為 magic property
      E.g. ``__init__``, ``__import__`` 或 ``__file__``
      請依照文檔描述來使用這些命名,千萬不要自己發明

規範性:命名約定 (Prescriptive: Naming Conventions)

需要避免的命名 (Names to Avoid)
單個字符的變數
不要使用和數字無法區別開的,像是小寫 l,大寫 O 或大寫 I

module/package/Class/Type 命名
  • module
    • 對應到文件名
    • 命名短
    • 全小寫
    • 可使用下劃線
    • C/C++ module 以下劃線開頭(e.g. _sociket)
  • package
    • 命名短
    • 全小寫
    • 不應使用下劃線
  • Class 
    • 單詞字母大寫(CapWords)
  • Type
    • 單詞字母大寫(CapWords)
    • 後綴建議加上 _co or _contra
      from typing import TypeVar
      
      VT_co = TypeVar('VT_co', covariant=True)
      KT_contra = TypeVar('KT_contra', contravariant=True)
      
異常命名 (Exception Names)
  • 同 class 命名
  • 加上 Error 的前綴

全域變量命名 (Global Variable Names)
  • 假設只在一個 module 中
  • 避免 from M import *
    • 使用 __all__ 機制
    • 採用下劃線前綴的舊約定來命名非公開全域變量

函數命名 (Function Names)
  • 全小寫
  • 可使用下劃線提高可讀性

函數和方法參數 (Function and method arguments)
  • instance 第一參數必定是 self
  • method  第一參數必定是 cls
  • 事實上,這只是約定俗成的名字,不一定需是 self or cls
  • 和保留關鍵字衝突時,使用下劃線結尾的命名,如 class_ 或 使用同義詞
方法命名和實例變量 (Method Names and Instance Variables)
  • 全小寫單詞
  • 使用下劃線提高可讀性
  • 一個下劃線開頭
    • 只對非公開方法和變數命名
  • 兩個下劃線開頭
    • 觸發 Python 的命名修飾機制
    • 只用來避免與子類屬性的命名衝突

常數 (Constants)
  • 在 module 定義的
  • 全大寫
  • 用下劃線將單詞分開
    • MAX_OVERFLOW 和 TOTAL

繼承設計 (Designing for inheritance)
  • 決定 class 的 method 和 property 應是公開的還是非公開的
    • 有疑慮的話,請選擇非公開的;因為之後將非公開屬性變為公開屬性要容易些
  • 公開屬性
    • 開頭不該有下劃線 
    • 和保留關鍵字衝突,後綴加上一個下劃線
    • 僅公開屬性名字,不要公開複雜的調用或設值方法
      • 使用 @property
      • 對計算量大的運算避免使用 @property
  • 非公開屬性
    • 前綴加上兩個下劃線並且結尾處沒有下劃線,會觸發 Python 命名修飾算法

公開和內部接口 (Public and internal interfaces)
  • module 應該在 __all__ 中明確定義公開的 API 
  • __all__ = [] 表示 module 無公開 API
  • 即使正確設置 __all__
    內部接口如 package,module,class,function,property 或其他命名,也應以一個下劃線開頭

設計建議 (Programming Recommendations)

  • 兼顧所有 python 的平台
    • 像是字串處理 a += b or a = a + b,在 CPython 只適用部分 type
      使用 ''.join() 取代 
  • 與 None 比較
    • 使用 is 或 is not 
    • 禁止使用 ==
  • 檢查是否不為 None,必須使用 is not
    以免跟 空 list 或其他物件搞混
    Yes: if x is not None
    No:  if x
    
  • 把 not 放在 is 後面
    正確方式
    if foo is not None:
    

    錯誤方式
    if not foo is None:
    
     
  • 若 class 有大小之分
    • 建議實現 __eq__ , __ne__ , __lt__ , __le__ , __gt__ , __ge__
      而不是另外寫 method 實現
  • 宣告 fucntion
    • 總是使用 def,debug 訊息較為明確
      正確方式
      def f(x): return 2*x
      

      錯誤方式
      f = lambda x: 2*x
      
  • Exception
    • 清楚明白什麼錯誤該怎麼處理
    • 嘗試描述什麼造成的問題
      • 格式為 raise X from Y 且保留原始的 traceback
      • 使用者需知道發生什麼錯誤
    • try/except 儘可能簡單明瞭
      正確方式
      try:
          value = collection[key]
      except KeyError:
          return key_not_found(key)
      else:
          return handle_value(value)
      

      錯誤方式
      try:
          # 太廣泛
          return handle_value(collection[key])
      except KeyError:
          # handle_value() 也可能有 KeyError
          return key_not_found(key)
      
    • 自定 Exception
      • 繼承自 Exception 而不是 BaseException
      • Class naming 加入前綴 Error
    • bare except:
      • 儘可能不只使用 except:,而是指定 Exception
        try:
            import platform_specific_module
        except ImportError:
            platform_specific_module = None
        
      • 因 SystemExit 和 KeyboardInterrupt 皆滿足,導致很難用 Control-C 中斷
      • 用 except Exception: 取代
      • 等同 except BaseException:
  • 儘量使用 with 呼叫 local resource
    • 像是讀寫檔,確保即時清理,try/finally 也是可以
    • 明確知道出 with 要關閉的是什麼
      正確方式
      with conn.begin_transaction():
          do_stuff_in_transaction(conn)
      

      錯誤方式
      with conn:
          do_stuff_in_transaction(conn)
      
      後者會將 conn 關掉,前者則是將 begin_transaction 回傳的關掉
  • 若有 return 的 function,若無值也需回傳 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
    • 使用自帶的 methods 而不是 string module 的 function
    • 別在結尾加很多空白,因為肉眼看不見,但處理會有問題,像互相比較
    • 使用 ''.startswith() 跟 ''.endswith()  檢查開頭與結尾,較簡潔又少錯誤
          Yes: if foo.startswith('bar'):
          No:  if foo[:3] == 'bar':
      
  • 比較 type 需用 isinstance()
    Yes: if isinstance(obj, int):
    
    No:  if type(obj) is type(1):
    
     
  • 對於 sequences, (strings, lists, tuples),空的就是 False
    Yes: if not seq:
         if seq:
    
    No: if len(seq):
        if not len(seq):
    
  • 別對 boolean 使用 ==
    Yes:   if greeting:
    No:    if greeting == True:
    Worse: if greeting is True:
    

參考

Python代碼風格指南(一)代碼設計(PEP8中文翻譯)
PEP8 Python 编码规范整理
python代码风格指南:pep8 中文翻译

留言