Google 內部的 Python 代碼風格指南

**來自:Why GitHub? **

鏈接:https://github.com/shendeguize/GooglePythonStyleGuideCN

這是一位朋友翻譯的 Google Python 代碼風格指南,很全面。可以作爲公司的 code review 標準,也可以作爲自己編寫代碼的風格指南。希望對你有幫助。

Translator: shendeguize@github

Link: https://github.com/shendeguize/GooglePythonStyleGuideCN

本翻譯囿於水平,可能有不準確的地方,歡迎指出,謝謝大家

1、背景

Python 是谷歌主要使用的動態語言,本風格指導列舉了使用 Python 編程時應該做和不該做的事項 (dos & don'ts)

爲了幫助你正確地組織代碼, 我們編寫了一個 Vim 的設置文件. 對於 Emacs, 默認設置即可.

許多團隊使用 yapf 自動格式工具來避免格式爭議

2、Python 語言規則

2.1 Lint

對代碼使用pylint

2.1.1Definition(以下都譯爲定義)

pylint是一個用於在 Python 代碼中發現 bug 和代碼風格問題的工具,,pylint查找那些常在非動態語言 (例如 C 或 C++) 編譯器中捕獲的問題. 由於 Python 是動態語言, 一些警告可能不正確, 不過應該非常少有錯誤警告.

2.1.2 Pros

能夠發現一些易被遺漏的錯誤, 類似拼寫錯誤, 調用早於聲明等等.

2.1.3 Cons

pylint並不完美, 爲了更好的利用工具, 我們有時候需要

a. Write around it(適配上下文風格)

b. 壓制一些警告

c. 優化工具

2.1.4 Decision(以下都譯爲建議)

確保對代碼應用pylint

如果一些警告是不合適的, 就抑制這些警告, 這是爲了讓其他警告不會被隱藏. 爲了壓制警告, 可以設置行級別的註釋:

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

pylint警告包含標識名 (empty-docstring), 谷歌專有的警告以g-開頭.

如果抑制警告的原因在標識名稱中表述不夠清晰, 請額外添加註解.

用這種方式來抑制警告的優點是我們能夠簡單地找到抑制的警告並且重新訪問這些警告.

可以通過下述方式來獲得pylint警告列表:

pylint --list-msgs

用下述方式來獲取某個特定消息的更多具體信息:

pylint --help-msg=C6409

優先使用pylint: disable而非舊方法 (pylint: disable-msg) 如果要抑制由於參數未使用的警告, 可以在函數開頭 del, 並註釋爲什麼要刪除這些未使用參數, 僅僅一句 "unused" 是不夠的:

def viking_cafe_order(spam, beans, eggs=None):
    del beans, eggs  # Unused by vikings.
    return spam + spam + spa

其他可以用來抑制警告的方式包括用'_'作爲未使用參數的標識, 在參數名前增加'unused_', 或者分配這些參數到'_'. 這些方式是可以的, 但是已經不鼓勵繼續使用. 前兩種方式會影響到通過參數名傳參的調用方式, 而最後一種並不能保證參數確實未被使用.

2.2 Imports

只在 import 包和模塊的時候使用import, 而不要應用在單獨的類或函數.(這一條對於 typing_module 有特別的意外)

2.2.1 定義

一個模塊到另一個模塊之間共享代碼的複用性機制

2.2.2 Pros

命名空間管理約定簡單, 每個標識的源都一致性地被指明瞭. 例如x.Obj表示Obj是在模塊x中定義的

2.2.3 Cons

模塊名可能會有衝突, 一些模塊名可能很長, 比較不方便

2.2.4 建議

sound.effects.echo爲例:

from sound.effects import echo...echo.EchoFilter(input, output, delay=0.7, atten=4)

不要使用相對引用,即便在同一包內,也使用完整包名 import, 這有助於避免無意重複 import 包.

從 typing module 和 six.moves module import 不適用上述規則

2.3 包

每一模塊都要從完整路徑 import

2.3.1 Pros

能夠避免模塊名衝突以及由於模塊搜索路徑與作者預期不符而造成的錯誤引用. 讓查找模塊更簡單.

2.3.2 Cons

讓部署代碼時有些困難, 因爲包架構也需要賦值, 不過對於現在的部署機制而言, 這其實不是問題.

2.3.3 建議

所有的新代碼都要從完整包名來 import 模塊

import 示例應該像這樣:

Yes:

# Reference absl.flags in code with the complete name (verbose).
# 在代碼中使用完整路徑調用absl.flags
import absl.flagsfrom doctor.who import jodie

FLAGS = absl.flags.FLAGS
# Reference flags in code with just the module name (common).
# 在代碼中只用包名來調用flags
from absl import flagsfrom doctor.who import jodie

FLAGS = flags.FLAGS

No:(假設文件在doctor/who中,jodie.py也在這裏)

# Unclear what module the author wanted and what will be imported.  The actual
# import behavior depends on external factors controlling sys.path.
# Which possible jodie module did the author intend to import?
# 不清楚作者想要哪個包以及最終import的是哪個包,
# 實際的import操作依賴於受到外部參數控制的sys.path
# 那麼哪一個可能的jodie模塊是作者希望import的呢?
import jodie

不應該假設主代碼所在路徑被包含在sys.path中, 即使有些時候可以 work. 在上一例代碼中, 我們應該認爲import jodie指的是 import 一個叫做jodie的第三方包或者頂級目錄中的jodie, 而非一個當前路徑的jodie.py

2.4 異常

異常處理是允許使用的, 但使用務必謹慎

2.4.1 定義

異常是一種從正常代碼段控制流中跳出以處理錯誤或者其他異常條件的手段.

2.4.2 Pros

正常代碼的控制流時不會被錯誤處理代碼影響的. 異常處理同樣允許在某些情況下, 控制流跳過多段代碼, 例如在某一步從 N 個嵌入函數返回結果而非強行延續錯誤代碼.

2.4.3 Cons

可能會讓控制流變的難於理解, 也比較容易錯過調用庫函數的報錯.

2.4.4 建議

異常必定遵循特定條件:

不要使用assert來片段公共結構參數值.assert是用來確認內部計算正確性也不是用來表示一些預期外的事件發生的. 如果異常是後續處理要求的, 用raise語句來處理, 例如:

Yes:

def connect_to_next_port(self, minimum):
"""Connects to the next available port.

Args:
    minimum: A port value greater or equal to 1024.

Returns:
    The new minimum port.

Raises:
    ConnectionError: If no available port is found.
"""
if minimum < 1024:
    # Note that this raising of ValueError is not mentioned in the doc
    # string's "Raises:" section because it is not appropriate to
    # guarantee this specific behavioral reaction to API misuse.
    # 注意拋出ValueError這件事是不在docstring中的Raises中提及, 因爲這樣並適合保障對於API誤用的特殊反饋
    raise ValueError('Minimum port must be at least 1024, not %d.' % (minimum,))
port = self._find_next_open_port(minimum)
if not port:
    raise ConnectionError('Could not connect to service on %d or higher.' % (minimum,))
assert port >= minimum, 'Unexpected port %d when minimum was %d.' % (port, minimum)
return port

No:

def connect_to_next_port(self, minimum):
"""Connects to the next available port.

Args:
    minimum: A port value greater or equal to 1024.

Returns:
    The new minimum port.
"""
assert minimum >= 1024, 'Minimum port must be at least 1024.'
port = self._find_next_open_port(minimum)
assert port is not None
return port
try:
    raise Error()
except Error as error:
    pass

2.5 全局變量

避免全局變量

2.5.1 定義

在模塊級別或者作爲類屬性聲明的變量

2.5.2 Pros

有些時候有用

2.5.3 Cons

在 import 的過程中, 有可能改變模塊行爲, 因爲在模塊首次被引入的過程中, 全局變量就已經被聲明

2.5.4 建議

避免全局變量

作爲技術變量, 模塊級別的常量是允許並鼓勵使用的. 例如MAX_HOLY_HANDGRENADE_COUNT = 3, 常量必須由大寫字母和下劃線組成, 參見下方命名規則

如果需要, 全局變量需要在模塊級別聲明, 並且通過在變量名前加_來使其對模塊內私有化. 外部對模塊全局變量的訪問必須通過公共模塊級別函數, 參見下方命名規則

2.6 內嵌 / 局部 / 內部 類和函數

內嵌局部函數或類在關閉局部變量時是可以的. 內部類意識可用的.(譯註: 這裏我的理解是當內嵌局部函數或類是和局部變量在同一個封閉作用域內是可以的.)

2.6.1 定義

類可以在方法, 函數, 類內定義. 函數可以在方法或函數內定義. 內嵌函數對封閉作用域的變量具有隻讀訪問權限.

2.6.2 Pros

允許定義只在非常有限作用域內可用的工具類或工具函數. Very ADT-y(?? 符合抽象數據類型要求???), 通常用於實現裝飾器

2.6.3 Cons

內嵌或局部類的實例是不能被 pickle 的, 內嵌函數或類是不能被直接測試的. 嵌套會讓外部函數更長並且更難讀懂.

2.6.4 建議

除了一些特別聲明, 這些內嵌 / 局部 / 內部類和函數都是可以的. 避免內嵌函數或類除了需要關閉一個局部值的時候.(譯者理解可能是除了將局部變量封閉在同一個作用域的情況以外). 不要把一個函數轉爲內嵌指示爲了避免訪問. 在這種情況下, 把函數置於模塊級別並在函數名前加_以保證測試是可以訪問該函數的.

2.7 列表推導和生成器表達式

在簡單情況下是可用的

2.7.1 定義

List, Dict 和 Set 推導生成式以及生成器表達式提供了一個簡明有效的方式來生成容器和迭代器而不需要傳統的循環,map(),filter()或者lambda表達式

2.7.2 Pros

簡單地推導表達比其他的字典, 列表或集合生成方法更加簡明清晰. 生成器表達式可以很有效率, 因爲完全避免了生成列表.

2.7.3 Cons

負載的推導表達式或生成器表達式很難讀懂

2.7.4 建議

簡單情況下使用時可以的. 每個部分 (mapping 表達式, filter 表達式等) 都應該在一行內完成. 多個 for 條款或者 filter 表達式是不允許的. 當情況變得很複雜的適合就使用循環.

Yes:

result = [mapping_expr for value in iterable if filter_expr]

result = [{'key': value} for value in iterable
          if a_long_filter_expression(value)]

result = [complicated_transform(x)
          for x in iterable if predicate(x)]

descriptive_name = [
    transform({'key': key, 'value': value}color='black')
    for key, value in generate_iterable(some_input)
    if complicated_condition_is_met(key, value)
]

result = []
for x in range(10):
    for y in range(5):
        if x * y > 10:
            result.append((x, y))

return {x: complicated_transform(x)
        for x in long_generator_function(parameter)
        if x is not None}

squares_generator = (x**2 for x in range(10))

unique_names = {user.name for user in users if user is not None}

eat(jelly_bean for jelly_bean in jelly_beans
    if jelly_bean.color == 'black')

No:

result = [complicated_transform(
          x, some_argument=x+1)
          for x in iterable if predicate(x)]

result = [(x, y) for x in range(10) for y in range(5) if x * y > 10]

return ((x, y, z)
        for x in range(5)
        for y in range(5)
        if x != y
        for z in range(5)
        if y != z)

2.8 默認迭代器和運算符

對支持默認迭代器和雲算法的類型例如列表, 字典和文件等使用它們

2.8.1 定義

容器類型 (例如字典, 列表等) 定義了的默認的迭代器和成員檢查運算符.

Pros

默認迭代器和操作符是簡單有效的, 能夠直接不需額外調用方法地表達操作. 使用默認操作符的函數是通用的. 能被用於任何支持這些操作的類型.

Cons

不能通過方法名來分辨類型, 例如has_key()意味着字典, 當然這也是一種優勢.

建議

對於支持的類型諸如列表, 字典和文件, 使用默認迭代器和操作符. 內置類型同樣定義了迭代器方法. 優先使用這些方法而非那些返回列表的方法. 除非能夠確定在遍歷容器的過程中不會改變容器. 不要使用 Python 2 專有迭代方法除非必要.

Yes:

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

No:

for key in adict.keys(): ...
if not adict.has_key(key): ...
for line in afile.readlines(): ...
for k, v in dict.iteritems(): ...

2.9 生成器

需要時使用生成器

2.9.1 定義

生成器函數返回一個迭代器, 每次執行yield語句的時候生成一個值. 在生成一個值之後, 生成器函數的運行被掛起直到需要下一個值.

2.9.2 Pros

簡化代碼, 因爲局部變量和控制流在每次調用時被保留, 生成器相比於一次性生成整個一個列表值要更節省內存.

2.9.3 Cons

2.9.4 建議

建議使用. 在生成器函數的文檔字符串中使用 "Yields:" 而非 "Returns:"

2.10 Lambda 表達式

單行代碼時是可以的

2.10.1 定義

lambda 在一個表達式內定義了匿名函數, 而不在語句裏. lambda 表達式常被用於定義高階函數 (例如map()filter()) 使用的回調函數或者操作符.

2.10.2 Pros

方便

2.10.3 Cons

比局部函數更難讀懂和 debug, 匿名意味着堆棧跟蹤更難懂. 表達性受限因爲 lambda 函數只包含一個表達式

2.10.4 建議

對於單行代碼而言, 可以使用 lambda 表達式. 如果lambda表達式內的代碼超過 60-80 個字符, 最好定義成爲常規的內嵌函數.

對於一般的操作諸如乘法, 使用operator模塊內置函數而非重新定義匿名函數, 例如使用operator.mul而非lambda x,y: x * y

2.11 條件表達式

簡單情況下可以使用.

2.11.1 定義

條件表達式 (也稱爲三元運算符) 是一種更短替代 if 語句的機制. 例如x = 1 if cond else 2

2.11.2 Pros

相對於 if 語句更短也更方便

2.11.3 Cons

比 if 語句可能更難讀懂, 當表達式很長的時候條件部分可能很難定位.

2.11.4 建議

簡單情況可以使用. 每個部分 (真值表達式, if 表達式, else 表達式) 必須在一行內完成. 如果使用條件表達式很富的時候使用完整的 if 語句.

Yes:

one_line = 'yes' if predicate(value) else 'no'
slightly_split = ('yes' if predicate(value)
                  else 'no, nein, nyet')
the_longest_ternary_style_that_can_be_done = (
    'yes, true, affirmative, confirmed, correct'
    if predicate(value)
    else 'no, false, negative, nay')

No:

bad_line_breaking = ('yes' if predicate(value) else
                     'no')portion_too_long = ('yes'
                    if some_long_module.some_long_predicate_function(
                        really_long_variable_name)
                    else 'no, false, negative, nay')

2.12 默認參數值

大多數情況下都 OK

2.12.1 定義

在函數參數列表的最後可以爲變量設定值, 例如def foo(a, b=0):. 如果foo在調用時只傳入一個參數, 那麼b變量就被設定爲 0, 如果調用時傳入兩個參數, 那麼b就被賦予第二個參數值.

2.12.2 Pros

通常一個函數可能會有大量默認值, 但是很少會有需要修改這些默認值的時候. 默認值就提供了一個很簡單滿足上述情況的方式, 而不需要爲這些少見的情況重新定義很多函數. 因爲 Python 不支持重載方法或函數, 默認參數是一個很簡單的方式來 "假重載" 行爲.

2.12.3 Cons

默認參數在模塊加載時就被複制. 這在參數是可變對象 (例如列表或字典) 時引發問題. 如果函數修改了這些可變對象(例如向列表尾添加元素). 默認值就被改變了.

2.12.4 建議

使用時請注意以下警告 ---- 在函數或方法定義時不要將可變對象作爲默認值.

Yes:

def foo(a, b=None):
    if b is None:
        b = []
def foo(a, b: Optional[Sequence] = None):
    if b is None:
        b = []
def foo(a, b: Sequence = ()):  # Empty tuple OK since tuples are immutable 空元組是也不可變的
    ...

No:

def foo(a, b=[]):
    ...
def foo(a, b=time.time()):  # The time the module was loaded??? 模塊被加載的時間???
    ...
def foo(a, b=FLAGS.my_thing):  # sys.argv has not yet been parsed... sys.argv還未被解析
    ...
def foo(a, b: Mapping = {}):  # Could still get passed to unchecked code 仍可傳入未檢查的代碼(此處翻譯可能有誤)
    ...

2.13 屬性

使用屬性可以通過簡單而輕量級的訪問器和設定器方法來訪問或設定數據.

2.13.1 定義

一種裝飾器調用來在計算比較輕量級時作爲標準的屬性訪問來獲取和設定一個屬性的方式

2.13.2 Pros

對於簡單的屬性訪問, 減少顯式的 get 和 set 方法能夠提升可讀性. 允許惰性計算. 被認爲是一種 Python 化的方式來維護類接口. 在表現上, 當直接對變量的訪問更合理時, 允許屬性繞過所需的瑣碎的訪問方法.

2.13.3 Cons

在 Python2 中必須繼承於object, 可能會隱藏像是操作符重載之類的副作用. 對於子類而言, 屬性可能有些迷惑性.

2.13.4 建議

在通常會有簡單而且輕量級的訪問和設定方法的新代碼裏使用屬性來訪問或設定數據. 屬性在創建時被@property裝飾, 參加裝飾器

如果屬性本身未被重寫, 帶有屬性的繼承可能不夠明晰, 因而必須確保訪問方法是被間接訪問的, 來確保子類的方法重載是被屬性調用的 (使用 Template Method DP, 譯者: 應是模板方法設計模式).

Yes:

class Square(object):
    """A square with two properties: a writable area and a read-only perimeter.

    To use:
    >>> 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

    @property
    def area(self):
        """Area of the square."""
        return self._get_area()

    @area.setter
    def area(self, area):
        return self._set_area(area)

    def _get_area(self):
        """Indirect accessor to calculate the 'area' property."""
        return self.side ** 2

    def _set_area(self, area):
        """Indirect setter to set the 'area' property."""
        self.side = math.sqrt(area)

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

2.14 True/False 表達式

只要可能, 就使用隱式 False 的 if 語句

2.14.1 定義

在布爾環境下, Python 對某些值判定爲 False, 一個快速的經驗規律是所有 "空" 值都被認爲是 False, 所以0, None, [], {}, ''的布爾值都是 False

2.14.2 Pros

使用 Python 布爾類型的條件語句可讀性更好而且更難出錯, 大多數情況下, 這種方式也更快.

2.14.3 Cons

對於 C/C++ 開發者而言可能有些奇怪

建議

如果可能的話, 使用隱式 False. 例如使用if foo:而非if foo != []:下面列舉了一些你應該牢記的警告:

Yes:

if not users:
    print('no users')

if foo == 0:
    self.handle_zero()

if i % 10 == 0:
    self.handle_multiple_of_ten()

def f(x=None):
    if x is None:
        x = []

No:

if len(users) == 0:
    print('no users')

if foo is not None and not foo:
    self.handle_zero()

if not i % 10:
    self.handle_multiple_of_ten()

def f(x=None):
    x = x or []

2.15 棄用的語言特性

儘可能利用字符串方法而非string模塊. 使用函數調用語法而非apply. 在函數參數本就是一個行內匿名函數的時候, 使用列表推導表達式和 for 循環而非filtermap

2.15.1 定義

當前 Python 版本提供了人們普遍更傾向的構建方式.

2.15.2 建議

我們不使用任何不支持這些特性的 Python 版本, 因而沒有理由不使用新方式.

Yes:

words = foo.split(':')

[x[1] for x in my_list if x[2] == 5]

map(math.sqrt, data)    # Ok. No inlined lambda expression. 可以,沒有行內的lambda表達式

fn(*args, **kwargs)

No:

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

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

apply(fn, args, kwargs)

2.16 詞法作用域

可以使用

2.16.1 定義

一個內嵌 Python 函數可以引用在閉包命名空間內定義的變量, 但是不能對其複製. 變量綁定是解析到使用詞法作用域的, 即基於靜態程序文本. 任何對塊內命名的賦值都會讓 Python 將對於這個命名的引用都作爲局部變量, 即使在使用先於賦值的情況下也是. 如果有全局聲明, 這個命名就會被認爲是全局變量.

一個使用這個特性的例子是:

def get_adder(summand1):
    """Returns a function that adds numbers to a given number."""
    def adder(summand2):
        return summand1 + summand2

    return adder

2.16.2 Pros

經常可以讓代碼更簡明優雅, 尤其會讓有經驗的 Lisp 和 Scheme(以及 Haskell 和 ML 還有其他) 的程序要很舒服.

2.16.3 Cons

可能會導致令人迷惑的 bug 例如這個基於 PEP-0227 的例子.

i = 4
def foo(x):
    def bar():
        print(i, end='')
    # ...
    # A bunch of code here
    # ...
    for i in x:  # Ah, i *is* local to foo, so this is what bar sees i對於foo來說是局部變量,所以在這裏就是bar函數所獲取的值
        print(i, end='')
    bar()

所以foo([1, 2, 3])會打印1 2 3 3而非1 2 3 4.

2.16.4 建議

可以使用

2.17 函數和方法裝飾器

在明顯有好處時, 謹慎明智的使用,避免@staticmethod,控制使用@classmethod

2.17.1 定義

函數和方法裝飾器 (也就是@記號). 一個常見的裝飾器是@property, 用於將普通方法轉換成動態計算屬性. 然而裝飾器語法也允許用戶定義裝飾器, 尤其對於一些函數my_decorator如下:

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

是等效於

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

2.17.2 Pros

能夠優雅的對方法進行某種轉換, 而該轉換可能減少一些重複代碼並保持不變性等等.

2.17.3 Cons

裝飾器可以對函數的參數和返回值任意操作, 導致非常隱形的操作行爲. 此外, 裝飾器在 import 的時候就被執行, 裝飾器代碼的實效可能非常難恢復.

2.17.4 建議

在有明顯好處的地方謹慎地使用裝飾器. 裝飾器應該和函數遵守相同的 import 和命名指導規則. 裝飾器的文檔應該清晰地聲明該函數爲裝飾器函數. 並且要爲裝飾器函數編寫單元測試.

避免裝飾器自身對外部的依賴,(如不要依賴於文件, socket, 數據庫連接等等), 這是由於在裝飾器運行的時候 (在 import 時, 可能從pydoc或其他工具中)這些外部依賴可能不可用. 一個被傳入有效參數並調用的裝飾器應該 (儘可能) 保證在任何情況下都可用.

裝飾器是一種特殊的 "頂級代碼", 參見 main

永遠不要使用@staticmethod, 除非不得不整合一個 API 到一個已有的庫, 應該寫一個模塊等級的函數.

只在寫一個命名的構造器或者一個類特定的, 修改必要的全局狀態 (例如進程緩存等) 的流程時使用@classmethod.

2.18 線程

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

儘管 Python 內置數據類型例如字典等似乎有原子性操作, 仍有一些罕見情況下, 他們是非原子的 (比如, 如果__hash__或者__eq__被實現爲 Python 方法), 就不應該依賴於這些類型的原子性. 也不應該依賴於原子變量賦值 (因爲這依賴於字典)

優先使用 Queue 模塊的Queue類來作爲線程之間通訊數據的方式. 此外, 要是用 threading 模塊和其 locking primitives(鎖原語). 瞭解條件變量的合理用法以便於使用threading.Condition而非使用更低級的鎖.

2.19 過於強大的特性

儘量避免使用

2.19.1 定義

Python 是一種非常靈活的語言並且提供了很多新奇的特性, 諸如定製元類, 訪問字節碼, 動態編譯, 動態繼承, 對象父類重定義, import hacks, 反射 (例如一些對於getattr()的應用), 系統內置的修改等等.

2.19.2 Pros

這些是非常強大的語言特性, 可以讓程序更緊湊

2.19.3 Cons

使用這些新特性是很誘人的. 但是並不絕對必要, 它們很難讀很難理解. 也很難 debug 那些在底層使用了不常見的特性的代碼. 對於原作者而言可能不是這樣, 但是再次看代碼的時候, 可能比更長但是更直接的代碼要難.

2.19.4 定義

避免在代碼中使用這些特性.

內部使用這些特性的標準庫和類是可以使用的 (例如abc.ABCMeta,collections.namedtuple, 和enum)

2.20 新版本 Python: Python3 和從__future__import

Python3 已經可用了 (譯者: 目前 Python2 已經不受支持了), 儘管不是每個項目都準備好使用 Python3, 所有的代碼應該兼容 Python3 並且在可能的情況下在 Python3 的環境下測試.

2.20.1 定義

Python3 是 Python 的重大改變, 儘管現有代碼通常是 Python2.7 寫成的, 但可以做一些簡單的事情來讓代碼更加明確地表達其意圖, 從而可以讓代碼更好地在 Python3 下運行而不用調整.

2.20.2 Pros

在考慮 Python3 編寫的代碼更清晰明確,一旦所有依賴已就緒,就可以更容易在 Python3 環境下運行.

2.20.3 Cons

一些人會認爲默認樣板有些醜, import 實際不需要的特性到模塊中是不常見的.

2.20.4 建議

from future imports

鼓勵使用from __future__ import語句. 所有新代碼都應該包含下述代碼, 而現有代碼應該被更新以儘可能兼容:

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

如果你不太熟悉這些, 詳細閱讀這些: 絕對 import, 新的/除法行爲, 和print函數

請勿省略或移除這些 import, 即使在模塊中他們沒有在使用, 除非代碼只用於 Python3. 最好總是在所有的文檔中都有從 future 的 import, 來保證不會在有人使用在後續編輯時遺忘.

有其他的from __future__import 語句, 看喜好使用. 我們的建議中不包含unicode_literals因爲其並無明顯優勢, 這是由於隱式默認的編碼轉換導致其在 Python2.7 內很多地方被引入了, 必要時, 大多數代碼最好顯式的使用b''u''btyes 和 unicode 字符串表示.(譯者: 這段翻譯可能不準確)

The six, future, or past libraries

當項目需要支持 Python2 和 3 時, 根據需求使用 six,future 和 past.

2.21 帶有類型註釋的代碼

可以根據 PEP-484 對 Python3 代碼進行類型註釋, 並且在 build 時用類型檢查工具例如 pytype 進行類型檢查.

類型註釋可以在源碼中或 stub pyi file 中. 只要可能, 註釋就應寫在源代碼中. 對於第三方或拓展模塊使用 pyi 文件.

2.21.1 定義

類型註釋 (也稱爲 "類型提示") 是用於函數或方法參數和返回值的:

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

你也可以聲明用一個單獨的註釋來聲明變量的類型:

a = SomeFunc()  # type: SomeType

2.21.2 Pros

類型註釋提升代碼的可讀性和可維護性, 類型檢查會將很多運行錯誤轉化爲構建錯誤, 也減少了使用過於強力特性的能力.

2.21.3 Cons

需要不斷更新類型聲明, 對於認爲有效的代碼可能會報類型錯誤, 使用類型檢查可能減少使用過於強力特性的能力.

2.21.4 建議

強烈鼓勵在更新代碼的時候進行 Python 類型分析. 在對公共 API 進行補充和修改時, 包括 python 類型聲明並通過構建系統中的 pytype 進行檢查. 對 Python 來說靜態類型檢查比較新, 我們承認, 一些意料外的副作用 (例如錯誤推斷的類型) 可能拒絕一些項目的使用. 這種情況下, 鼓勵作者適當地增加一個帶有 TODO 或到 bug 描述當前不接搜的類型註釋的鏈接到 BUILD 文件或者在代碼內.

3、Python 代碼風格規範

3.1 分號

不要在行尾加分號,也不要用分號把兩行語句合併到一行

3.2 行長度

最大行長度是 80 個字符

超出 80 字符的明確例外:

不要使用反斜槓連接, 除非對於需要三層或以上的上下文管理器with語句

利用 Python 的 implicit line joining inside parentheses, brackets and braces(隱式行連接方法 -- 括號連接, 包括(), [], {}). 如果必要的話, 也可在表達式外面額外添加一對括號.

Yes:

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

當字符串不能在一行內完成時, 使用括號來隱式連接行:

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

在註釋內, 如有必要, 將長 URL 放在其本行內:

Yes:

# See details at
# http://www.example.com/us/developer/documentation/api/content/v2.0/csv_file_name_extension_full_specification.html

No:

# See details at
# http://www.example.com/us/developer/documentation/api/content/\
# v2.0/csv_file_name_extension_full_specification.html

在定義一個表達式超過三行或更多的with語句時, 可以使用反斜槓來分行. 對於兩行表達式, 使用嵌套with語句:

Yes:

with very_long_first_expression_function() as spam, \
     very_long_second_expression_function() as beans, \
     third_thing() as eggs:
    place_order(eggs, beans, spam, beans)

with very_long_first_expression_function() as spam:
    with very_long_second_expression_function() as beans:
        place_order(beans, spam)

No:

with VeryLongFirstExpressionFunction() as spam, \
     VeryLongSecondExpressionFunction() as beans:
    PlaceOrder(eggs, beans, spam, beans)

注意上述例子中的縮進, 具體參看縮進

在其他一行超過 80 字符的情況下, 而且 yapf 自動格式工具也不能使分行符合要求時, 允許超過 80 字符限制.

3.3 括號

括號合理使用

儘管不必要, 但是可以在元組外加括號. 再返回語句或者條件語句中不要使用括號, 除非是用於隱式的連接行或者指示元組.

Yes:

if foo:
    bar()
while x:
    x = bar()
if x and y:
    bar()
if not x:
    bar()
# For a 1 item tuple the ()s are more visually obvious than the comma.
onesie = (foo,)
return foo
return spam, beans
return (spam, beans)
for (x, y) in dict.items(): ...

No:

if (x):
    bar()
if not(x):
    bar()
return (foo)

3.4 縮進

縮進用 4 個空格

縮進代碼段不要使用製表符, 或者混用製表符和空格. 如果連接多行, 多行應垂直對齊, 或者再次 4 空格縮進 (這個情況下首行括號後應該不包含代碼).

Yes:

# Aligned with opening delimiter
# 和opening delimiter對齊(譯者理解是分隔符的入口,例如三種括號,字符串引號等)
foo = long_function_name(var_one, var_two,
                         var_three, var_four)
meal = (spam,
        beans)

# Aligned with opening delimiter in a dictionary
foo = {
    long_dictionary_key: value1 +
                         value2,
    ...
}

# 4-space hanging indent; nothing on first line
# 縮進4個空格,首行括號後無內容
foo = long_function_name(
    var_one, var_two, var_three,
    var_four)
meal = (
    spam,
    beans)

# 4-space hanging indent in a dictionary
foo = {
    long_dictionary_key:
        long_dictionary_value,
    ...
}

No:

# Stuff on first line forbidden
# 首行不允許有內容
foo = long_function_name(var_one, var_two,
    var_three, var_four)
meal = (spam,
    beans)

# 2-space hanging indent forbidden
foo = long_function_name(
  var_one, var_two, var_three,
  var_four)

# No hanging indent in a dictionary
foo = {
    long_dictionary_key:
    long_dictionary_value,
    ...
}

3.4.1 關於尾後逗號

關於在一序列元素中的尾號逗號, 只推薦在容器結束符號],)或者}和最後元素不在同一行時使用. 尾後逗號的存在也被用作我們 Python 代碼自動格式化工具 yapf 的提示, 在,最後元素之後出現的時候來自動調整容器元素到每行一個元素.

Yes:

golomb3 = [0, 1, 3]
golomb4 = [
    0,
    1,
    4,
    6,
]

No:

golomb4 = [
    0,
    1,
    4,
    6
]

3.5 空行

在頂級定義 (函數或類) 之間要間隔兩行. 在方法定義之間以及class所在行與第一個方法之間要空一行,def行後無空行, 在函數或方法內你認爲合適地方可以使用單空行.

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)
dict['key'] = list[index]

No:

spam (1)
dict ['key'] = list [index]

行尾不要加空格.

在賦值 (=), 比較 (==,<,>,!=,<>,<=,>=,in,not in,is,is not), 布爾符號 (and,or,not) 前後都加空格. 視情況在算術運算符 (+,-,*,/,//,%,**,@), 前後加空格

Yes:

x == 1

No:

x<1

在關鍵字名參數傳遞或定義默認參數值的時候不要在=前後加空格, 只有一個例外: 當類型註釋存在時在定義默認參數值時=前後加空格

Yes:

def complex(real, imag=0.0)return Magic(r=real, i=imag)
def complex(real, imag: float = 0.0)return Magic(r=real, i=imag)

No:

def complex(real, imag = 0.0)return Magic(r = real, i = imag)
def complex(real, imag: float=0.0)return Magic(r = real, i = imag)

不要用空格來做無必要的對齊, 因爲這會在維護時帶來不必要的負擔 (對於:.#,=等等).

Yes:

foo = 1000  # comment
long_name = 2  # comment that should not be aligned
dictionary = {
    'foo': 1,
    'long_name': 2,
}

No:

foo       = 1000  # comment
long_name = 2     # comment that should not be aligned

dictionary = {
    'foo'      : 1,
    'long_name': 2,
}

3.7 Shebang

大部分.py文件不需要從#!行來開始. 根據 PEP-394, 程序的主文件應該以#!/usr/bin/python2#!/usr/bin/python3起始

這行被用於幫助內核找到 Python 解釋器, 但是在導入模塊時會被 Python 忽略 / 只在會被直接運行的文件裏有必要寫.

3.8 註釋和文檔字符串

確保使用正確的模塊, 函數, 方法的文檔字符串和行內註釋.

3.8.1 文檔字符串

Python 使用_文檔字符串_來爲代碼生成文檔. 文檔字符串是包, 模塊, 類或函數的首個語句. 這些字符串能夠自動被__doc__成員方法提取並且被pydoc使用.(嘗試在你的模塊上運行pydoc來看看具體是什麼). 文檔字符串使用三重雙引號"""(根據 PEP-257). 文檔字符串應該這樣組織: 一行總結 (或整個文檔字符串只有一行) 並以句號, 問好或感嘆號結尾. 隨後是一行空行, 隨後是文檔字符串, 並與第一行的首個引號位置相對齊. 更多具體格式規範如下.

3.8.2 模塊

每個文件都應包含許可模板. 選擇合適的許可模板用於項目 (例如 Apache 2.0,BSD,LGPL,GPL)

文檔應該以文檔字符串開頭, 並描述模塊的內容和使用方法.

"""A one line summary of the module or program, terminated by a period.

Leave one blank line.  The rest of this docstring should contain an
overall description of the module or program.  Optionally, it may also
contain a brief description of exported classes and functions and/or usage
examples.

  Typical usage example:

  foo = ClassFoo()
  bar = foo.FunctionBar()
"""

3.8.3 函數和方法

在本節,"函數" 所指包括方法, 函數或者生成器.

函數應有文檔字符串, 除非符合以下所有條件:

文檔字符串應該包含足夠的信息以在無需閱讀函數代碼的情況下調用函數. 文檔字符串應該是敘事體 ("""Fetches rows from a Bigtable.""") 的而非命令式的 ("""Fetch rows from a Bigtable."""), 除了@property(應與 attribute 使用同樣的風格). 文檔字符串應描述函數的調用語法和其意義, 而非實現. 對比較有技巧的地方, 在代碼中使用註釋更合適.

覆寫了基類的方法可有簡單的文檔字符串向讀者指示被覆寫方法的文檔字符串例如"""See base class.""". 這是因爲沒必要在很多地方重複已經在基類的文檔字符串中存在的文檔. 不過如果覆寫的方法行爲實際上與被覆寫方法不一致, 或者需要提供細節 (例如文檔中表明額外的副作用), 覆寫方法的文檔字符串至少要提供這些差別.

一個函數的不同方面應該在特定對應的分節裏寫入文檔, 這些分節如下. 每一節都由以冒號結尾的一行開始, 每一節除了首行外, 都應該以 2 或 4 個空格縮進並在整個文檔內保持一致 (譯者建議 4 個空格以維持整體一致). 如果函數名和簽名足夠給出足夠信息並且能夠剛好被一行文檔字符串所描述, 那麼可以忽略這些節.

Args:

列出每個參數的名字. 名字後應有爲冒號和空格, 後跟描述. 如果描述太長不能夠在 80 字符的單行內完成. 那麼分行並縮進 2 或 4 個空格且與全文檔一致 (譯者同樣建議 4 個空格)

描述應該包含參數所要求的類型, 如果代碼不包含類型註釋的話. 如果函數容許*foo(不定長度參數列表) 或**bar(任意關鍵字參數). 那麼就應該在文檔字符串中列舉爲*foo**bar.

Returns:(或對於生成器是 Yields:)

描述返回值的類型和含義. 如果函數至少返回 None, 這一小節不需要. 如果文檔字符串以 Returns 或者 Yields 開頭 (例如"""Returns row from Bigtable as a tuple of strings.""") 或首句足夠描述返回值的情況下這一節可忽略.

Raises:

列出所有和接口相關的異常. 對於違反文檔要求而拋出的異常不應列出.(因爲這會矛盾地使得違反接口要求的行爲成爲接口的一部分)

def fetch_bigtable_rows(big_table, keys, other_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.
    """

3.8.4 類

類定義下一行應爲描述這個類的文檔字符串. 如果類有公共屬性, 應該在文檔字符串中的Attributes節中註明, 並且和函數的Args一節風格統一.

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__(self, likes_spam=False):
        """Inits SampleClass with blah."""
        self.likes_spam = likes_spam
        self.eggs = 0

    def public_method(self):
        """Performs operation blah."""

3.8.5 塊註釋和行註釋

最後要在代碼中註釋的地方是代碼技巧性的部分. 如果你將要在下次 code review 中揭示代碼. 應該現在就添加註釋. 在複雜操作開始前, 註釋幾行. 對於不夠明晰的代碼在行尾註釋.

# 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 if i is 0 or a power of 2.

爲了提升易讀性, 行註釋應該至少在代碼 2 個空格後, 並以#後接至少 1 個空格開始註釋部分.

另外, 不要描述代碼, 假定閱讀代碼的人比你更精通 Python(他只是不知道你試圖做什麼).

3.8.6 標點, 拼寫和語法

注意標點, 拼寫和語法, 寫得好的註釋要比寫得差的好讀.

註釋應當是和敘事性文本一樣可讀, 並具有合適的大小寫和標點. 在許多情況下, 完整的句子要比破碎的句子更可讀. 更簡短的註釋如行尾的註釋有時會不太正式, 但是應該全篇保持風格一致.

儘管被代碼審覈人員指出在應該使用分號的地方使用了逗號是很令人沮喪的, 將源代碼維護在高度清楚可讀的程度是很重要的. 合適的標點, 拼寫和語法能夠幫助達到這個目標.

3.9 類

如果類並非從其他基類繼承而來, 那麼就要明確是從object繼承而來, 即便內嵌類也是如此.

Yes:

class SampleClass(object):
    pass

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

class ChildClass(ParentClass):
    """Explicitly inherits from another class already."""

No:

class SampleClass:
    pass

class OuterClass:
    class InnerClass:
        pass

object類繼承保證了屬性能夠在 Python2 正確運行並且保護代碼在 Python3 下出現潛在的不兼容. 這樣也定義了 object 包括__new__,__init__,__delattr__,__getattribute__,__setattr__,__hash__,__repr__, 和__str__等默認特殊方法的實現.

3.10 字符串

使用format%來格式化字符串, 即使參數都是字符串對象, 也要考慮使用+還是%format.

Yes:

x = a + b
x = '%s, %s!' % (imperative, expletive)
x = '{}, {}'.format(first, second)
x = 'name: %s; score: %d' % (name, n)
x = 'name: {}; score: {}'.format(name, n)
x = f'name: {name}; score: {n}'  # Python 3.6+

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

避免使用++=操作符來在循環內累加字符串, 因爲字符串是不可變對象. 這會造成不必要的臨時變量導致運行時間以四次方增長而非線性增長. 應將每個字符串都記入一個列表並使用''.join來將列表在循環結束後連接 (或將每個子字符串寫入io.BytesIO緩存)

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)

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:

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

No:

Python("Why are you hiding your eyes?")
Gollum('The lint. It burns. It burns us.')
Gollum("Always the great lint. Watching. Watching.")

多行字符串多行字符串優先使用 """ 而非''', 當且只當對所有非文檔字符串的多行字符串都是用'''而且對正常字符串都使用'時纔可使用三單引號. docstring 不論如何必須使用"""

多行字符串和其餘代碼的縮進方式不一致. 如果需要避免在字符串中插入額外的空格, 要麼使用單行字符串連接或者帶有textwarp.dedent()的多行字符串來移除每行的起始空格.

No:

long_string = """This is pretty ugly.
Don't do this.
"""

Yes:

long_string = """This is fine if your use case can accept
    extraneous leading spaces."""

long_string = ("And this is fine if you can not accept\n" +
               "extraneous leading spaces.")

long_string = ("And this too is fine if you can not accept\n"
               "extraneous leading spaces.")

import textwrap

long_string = textwrap.dedent("""\
    This is also fine, because textwrap.dedent()
    will collapse common leading spaces in each line.""")

3.11 文件和 socket

當使用結束後顯式地關閉文件或 socket.

不必要地打開文件,socket 或其他類似文件的對象有很多弊端:

此外, 當文件或 socket 在文件對象被銷燬的同時被自動關閉的時候, 是不可能將文件的生命週期和文件狀態綁定的:

推薦使用 with 語句管理文件:

with open("hello.txt") as hello_file:
    for line in hello_file:
        print(line)

對於類似文件的對象, 如果不支持 with 語句的可以使用contextlib.closing():

import contextlib

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

3.12  TODO 註釋

對於下述情況使用TODO註釋: 臨時的, 短期的解決方案或者足夠好但是不完美的解決方案.

TODO註釋以全部大寫的字符串TODO開頭, 並帶有寫入括號內的姓名, email 地址, 或其他可以標識負責人或者包含關於問題最佳描述的 issue. 隨後是這裏做什麼的說明.

有統一風格的TODO的目的是爲了方便搜索並瞭解如何獲取更多相關細節.TODO並不是保證被提及者會修復問題. 因此在創建TODO註釋的時候, 基本上都是給出你的名字.

# TODO(kl@gmail.com): Use a "*" here for string repetition.
# TODO(Zeke) Change this to use relations.

如果TODO註釋形式爲 "未來某個時間點會做什麼事" 的格式, 確保要麼給出一個非常具體的時間點 (例如 "將於 2009 年 11 月前修復") 或者給出一個非常具體的事件(例如 "當所有客戶端都能夠處理 XML 響應時就移除此代碼").

3.13 import 格式

imports 應該在不同行. 例如:

Yes:

import os
import sys

No:

import os, sys

import 應集中放在文件頂部, 在模塊註釋和 docstring 後面, 模塊 globals 和常量前面. 應按照從最通用到最不通用的順序排列分組:

  1. Python 未來版本 import 語句, 例如:

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

    更多信息參看上文

  2. Python 標準基礎庫 import, 例如:

    import sys
  3. 第三方庫或包的 import, 例如:

    import tensorflow as tf
  4. 代碼庫內子包 import, 例如:

    from otherproject.ai import mind
  5. 此條已棄用: 和當前文件是同一頂級子包專用的 import, 例如:

    from myproject.backend.hgwells import time_machine

    在舊版本的谷歌 Python 代碼風格指南中實際上是這樣做的. 但是現在不再需要了.** 新的代碼風格不再受此困擾.** 簡單的將專用的子包 import 和其他子包 import 同一對待即可.

在每個組內按照每個模塊的完整包路徑的字典序忽略大小寫排序. 可以根據情況在每個節質檢增加空行.

import collectionsimport queueimport sys

from absl import appfrom absl import flagsimport bs4import cryptographyimport tensorflow as tf

from book.genres import scififrom myproject.backend.hgwells import time_machinefrom myproject.backend.state_machine import main_loopfrom otherproject.ai import bodyfrom otherproject.ai import mindfrom otherproject.ai import soul

# Older style code may have these imports down here instead:
# 舊版本代碼風格可能會採用下述import方式
# from myproject.backend.hgwells import time_machine
# from myproject.backend.state_machine import main_loop

3.14 語句

每行只有一條語句.

不過如果測試語句和結果能夠在一行內放下, 就可以放在一行內. 但是不允許將try/except語句和對應內容放於一行, 因爲try或者except都不能在一行內完成. 對於沒有 else 的 if 語句可以將if和對應內容合併到一行.

Yes:

if foo: bar(foo)

No:

if foo: bar(foo)
else:   baz(foo)

try:               bar(foo)
except ValueError: baz(foo)

try:
    bar(foo)
except ValueError: baz(foo)

3.15 訪問

對於瑣碎又不太重要的訪問函數, 應用公共變量來替代訪問函數, 以避免額外的程序調用消耗, 當添加了更多函數功能時, 使用property來保持連續性

此外, 如果訪問過於複雜, 或者訪問變量的消耗過大, 應該使用諸如get_foo()set_foo()之類的函數式訪問 (參考命名指南). 如果過去的訪問方式是通過屬性, 新訪問函數不要綁定到 property 上, 這樣使用 property 的舊方式就會失效, 使用者就會知道函數有變化.

3.16 命名

module_name,package_name,ClassName,method_name,ExceptionName,function_name,GLOBAL_CONSTANT_NAME,global_var_name,instance_var_name,function_parameter_name,local_var_name.

命名函數名, 變量名, 文件名應該是描述性的, 避免縮寫, 尤其避免模糊或對讀者不熟悉的縮寫. 並且不要通過刪減單詞內的字母來縮短.

使用.py作爲文件拓展名, 不要使用橫線.

3.16.1 要避免的名字:

3.16.4 命名約定

3.16.3 文件名

文件拓展名必須爲.py, 不可以包含-. 這保證了能夠被正常 import 和單元測試. 如果希望一個可執行文件不需要拓展名就可以被調用, 那麼建立一個軟連接或者一個簡單的 bash 打包腳本包括exec "$0.py" "$@".

3.16.4 Guido 的指導建議

qa4BY5

儘管 Python 支持通過雙下劃線__(即 "dunder") 來私有化. 不鼓勵這樣做. 優先使用單下劃線. 單下劃線更易於打出來、易讀、易於小的單元測試調用. Lint 的警告關注受保護成員的無效訪問.

3.17 Main

即便是一個用做腳本的 py 文件也應該是可以被 import 的, 而只用於 import 時, 也不應有執行了主函數的副作用. 主函數的功能應該被放在main()裏.

在 Python 中,pydoc和單元測試要求模塊是可 import 的. 所以代碼在主程序執行前應進行if __name__ == '__main__':檢查, 以防止模塊在 import 時被執行.

def main():
    ...

if __name__ == '__main__':
    main()

所有頂級代碼在模塊被 import 時執行. 因而要小心不要調用函數, 創建對象或者執行其他在執行pydoc時不應該被執行的操作.

3.18 函數長度

優先寫小而專一的函數.

長函數有時候是合適的, 故而函數長度沒有固定的限制. 但是超過 40 行的時候就要考慮是否要在不影響程序結構的前提下分解函數.

儘管長函數現在運行的很好, 但是在之後的時間裏其他人修改函數並增加新功能的時候可能會引入新的難以發現的 bug, 保持函數的簡短, 這樣有利於其他人讀懂和修改代碼.

在處理一些代碼時, 可能會發現有些函數長而且複雜. 不要畏懼調整現有代碼, 如果處理這個函數非常困難, 如難以對報錯 debug 或者希望在幾個不同的上下文中使用它, 那麼請將函數拆解成若干個更小更可控的片段.

3.19 類型註釋

3.19.1 基本規則

3.19.2 分行

遵循現有的縮進規範

標註類型後, 函數簽名多數都要是 "每行一個參數".

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, other_arg: Optional[MyLongType]
) -> Dict[OtherLongType, MyLongType]:
  ...

pylint 允許您將右括號移動到新行並與左括號對齊, 但這不太容易理解.

No:

def my_method(self,
              other_arg: Optional[MyLongType]
             ) -> Dict[OtherLongType, MyLongType]:
  ...

就像上面的例子一樣, 儘量不要分割類型註釋, 不過有時類型註釋太長無法放入一行,(那就儘量讓子註釋不要被分割).

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

如果某個命名和類型太長了, 考慮使用別名. 如果沒有其他解決方案, 在冒號後分行縮進 4 個空格.

Yes:

def my_function(
    long_variable_name:
        long_module_name.LongTypeName,
) -> None:
  ...

No:

def my_function(
    long_variable_name: long_module_name.
        LongTypeName,) -> None:
  ...

3.19.3 前置聲明

如果需要同一模塊內還未定義的類名, 例如需要類聲明內部的類, 或者需要在後續代碼中定義的類, 那麼使用類名的字符串來代替.

class MyClass(object):

  def __init__(self,
               stack: List["MyClass"]) -> None:

3.19.4 默認值

參考 PEP-008, 只有在同時需要類型註釋和默認值的時候在=前後都加空格

Yes:

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

No:

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

3.19.5 NoneType

在 Python 系統中NoneType是一等類型, 爲了方便輸入,NoneNoneType的別名. 如果一個參數可以是None, 那麼就需要聲明! 可以使用Union, 但如果只有一個其他類型, 那麼使用Optional.

顯式地使用Optional而非隱式地. PEP 484 的早期版本容許a: Text = None被解釋爲a: Optional[Text] = None. 但現在已經不推薦這樣使用了.

Yes:

def func(a: Optional[Text], b: Optional[Text] = None) -> Text:
  ...
def multiple_nullable_union(a: Union[None, Text, int]) -> Text
  ...

No:

def nullable_union(a: Union[None, Text]) -> Text:
  ...
def implicit_optional(a: Text = None) -> Text:
  ...

3.19.6 類型別名

可以對複雜類型聲明別名, 別名的名稱應爲 CapWorded, 如果只用於當前模塊, 應加下劃線私有化.

例如, 如果帶有模塊名的類型名過長:

_ShortName = module_with_long_name.TypeWithLongName
ComplexMap = Mapping[Text, List[Tuple[int, int]]]

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

3.19.7 忽略類型檢查

可以通過增加特殊行註釋# type: ignore來禁止類型檢查.

pytype對於明確的報錯有關閉選項 (類似於 lint):

# pytype: disable=attribute-error

3.19.8 對變量註釋類型

對變量標註類型如果內部變量很難或者不可能指向, 可以使用下述方式:

類型註釋:

在行尾增加以# type開頭的註釋

a = SomeUndecoratedFunction()  # type: Foo

註釋綁定:

在變量名和賦值之間用冒號和類型註明, 和函數參數一致.

a: Foo = SomeUndecoratedFunction()

3.19.9 元組和列表

不像是列表只能包含單一類型, 元組可以既只有一種重複類型或者一組不同類型的元素, 後者常用於函數返回.

a = [1, 2, 3]  # type: List[int]
b = (1, 2, 3)  # type: Tuple[int, ...]
c = (1, "2", 3.5)  # type: Tuple[int, Text, float]

3.19.10 TypeVars

Python 是有泛型的, 工廠函數TypeVar是通用的使用方式.

例子:

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

TypeVar 可以約束類型:

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

typing模塊預定義好的類型變量是AnyStr, 用於針對字符串可以是bytes也可爲unicode並且保持一致的多個類型註釋.

from typing import AnyStr
def check_length(x: AnyStr) -> AnyStr:
  if len(x) <= 42:
    return x
  raise ValueError()

3.19.11 字符串類型

註釋字符串的合適類型是基於 Python 版本的.

對於只有 Python3 的代碼, 使用str,Text可以用但是在選擇上保持一致.

對於 Python2 兼容的代碼, 用Text, 在一些很罕見的情況下,str可能可用. 當在不同 Python 版本之間返回值類型不同的時候通常是爲了照顧兼容性. 避免使用unicode, 因爲 Python3 中不存在.

No:

def py2_code(x: str) -> unicode:
  ...

對於處理二進制數據的代碼, 請使用bytes.

Yes:

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

對於 Python2 兼容, 處理文本數據 (Python 中strunicode,Python3 中str) 的代碼, 使用Text. 對於只有 Python3 的代碼, 優先使用str.

from typing import Text
...
def py2_compatible(x: Text) -> Text:
  ...
def py3_only(x: str) -> str:
  ...

如果既可以是 byte 也可以是文本, 那麼使用Union和合適的文本類型.

from typing import Text, Union
...
def py2_compatible(x: Union[bytes, Text]) -> Union[bytes, Text]:
  ...
def py3_only(x: Union[bytes, str]) -> Union[bytes, str]:
  ...

如果一個函數中所有的字符串類型始終一致, 例如前文例子中返回值類型和參數類型是一致的, 那麼使用AnyStr

像這樣寫能夠簡化代碼向 Python3 的遷移過程.

3.19.12 typing 的 import

對於從typing模塊 import 的類, 要 import 類本身. 明確的允許在一行內從typing模塊 import 多個特定的類, 如

from typing import Any, Dict, Optional

這種從typing模塊 import 的方式會向命名空間內增加額外項,typing中的任何命名都應該和關鍵字同等對待並且不在你的 Python 代碼中定義, typed or not(譯者推測文無論是否引入). 如果和已有的命名衝突, 使用import x as y來 import.

from typing import Any as AnyType

3.19.13 條件 import

只在運行時一定要避免進行類型檢查的情況下使用條件 import. 不鼓勵使用這種模式. 鼓勵使用其他替代方式諸如重構代碼以容許頂級 import.

只用於類型註釋的 import 可以被歸於if TYPE_CHECKING:代碼塊中.

import typing
if typing.TYPE_CHECKING:
    import sketch
def f(x: "sketch.Sketch"): ...

3.19.14 循環依賴

由於類型檢查引發的循環依賴是一種 code smell(代碼異味), 這樣的代碼應當被重構. 儘管技術上是可以保留循環引用的. build system(系統) 不允許這樣做因爲每個模塊都要依賴於其他模塊.

將造成循環依賴的模塊替換爲Any並賦予一個有意義的別名並使用從這個模塊導入的真實類名 (因爲任何Any的屬性都是Any). 別名的定義用和最後一行 import 用一行空行分隔.

from typing import Any

some_mod = Any  # some_mod.py imports this module.
...

def my_method(self, var: some_mod.SomeType) -> None:
  ...

3.19.15  泛型

當註釋的時候, 優先泛型類型專有類型參數, 否則泛型的參數會被認爲是Any.

def get_names(employee_ids: List[int]) -> Dict[int, Any]:
  ...
# These are both interpreted as get_names(employee_ids: List[Any]) -> Dict[Any, Any]
def get_names(employee_ids: list) -> Dict:
  ...

def get_names(employee_ids: List) -> Dict:
  ...

如果泛型最佳的參數類型是Any也將其顯式地表示出來. 但是在很多情況下TypeVar可能更合適.

def get_names(employee_ids: List[Any]) -> Dict[Any, Text]:
"""Returns a mapping from employee ID to employee name for given IDs."""
T = TypeVar('T')
def get_names(employee_ids: List[T]) -> Dict[T, Text]:
"""Returns a mapping from employee ID to employee name for given IDs."""

4、最後的話

如果你在編輯代碼, 花幾分鐘看看現有代碼然後決定好要使用哪種風格. 如果現有代碼在所有算術運算符兩側都加了空格, 那麼你也應該如此. 如果現有的註釋用井號組成了包圍框, 那麼你的註釋也應如此.

有代碼風格指南的目的是有一個編程的共識, 這樣人們能夠集中在內容而非形式上. 我們將通用的代碼風格指南公佈於此這樣人們就能瞭解這個共識 (譯者: 有巴別塔的意味.) 但是各自的代碼風格也很重要. 如果你添加的代碼與原有代碼看起來完全不一致,就會打亂讀者的閱讀節奏,避免這樣。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/YghgDFu8V2EWVlzxInW_0Q