Python良好的程式碼風格對於提升程式碼可讀性、可維護性至關重要。本文從PEP 8的程式碼行長度限制談起,探討了程式碼換行的最佳實踐,並介紹了Python的解封裝技術、高效建立相同元素列表的方法,以及使用str.join()進行字串拼接的技巧。此外,文章還說明瞭如何利用集合提升查詢效率,並詳細闡述了例外安全上下文和上下文管理器的使用方法,以及如何避免可變預設引數和延遲繫結閉包的常見陷阱。最後,文章從專案結構的角度出發,討論了模組和套件的組織方式,以及如何避免迴圈依賴、隱藏耦合和過度使用全域狀態等問題,以建立更清晰、更易於維護的Python專案。

撰寫優雅的Python程式碼

撰寫優雅且易於維護的Python程式碼需要遵循特定的風格與最佳實踐。本章節將探討Python的程式碼風格、常見慣用法以及提升程式碼可讀性的技巧。

行長度限制與程式碼換行

根據PEP 8的建議,Python程式碼的行長度應限制在80個字元以內。然而,不同的開發團隊可能會採用不同的限制標準,例如100個字元。不過,當需要在終端機環境下除錯程式碼時,80字元的限制能避免程式碼換行,從而提升可讀性。

程式碼換行範例

當邏輯行(logical line)超過設定的字元限制時,需要將其拆分成多個物理行(physical lines)。Python直譯器會將以反斜線(backslash)結尾的行與下一行合併。然而,這種方法可能因反斜線後的空白字元而導致錯誤。

# 不推薦的寫法
french_insult = \
"Your mother was a hamster, and \
your father smelt of elderberries!"

# 推薦的寫法
french_insult = (
    "Your mother was a hamster, and "
    "your father smelt of elderberries!"
)

內容解密:

  1. 使用括號將表示式包圍,能夠自然地進行換行。
  2. 這種寫法避免了反斜線可能帶來的問題。
  3. 提升了程式碼的可讀性與維護性。

解封裝(Unpacking)

Python允許在已知列表或元組長度的情況下,將其元素指定給多個變數。這種技術稱為解封裝。

解封裝範例

# 基本解封裝
filename, ext = "my_photo.orig.png".rsplit(".", 1)
print(filename, "is a", ext, "file.")

# 交換變數值
a, b = b, a

# 巢狀解封裝
a, (b, c) = 1, (2, 3)

# 擴充套件解封裝(Python 3)
a, *rest = [1, 2, 3]  # a = 1, rest = [2, 3]
a, *middle, c = [1, 2, 3, 4]  # a = 1, middle = [2, 3], c = 4

內容解密:

  1. 解封裝技術簡化了變數指定的過程。
  2. 能夠用於交換變數值、巢狀結構的處理,以及擴充套件解封裝。
  3. 這種技術使程式碼更加簡潔且易於理解。

建立相同元素的列表

可以使用Python列表的*運算元來建立包含相同不可變元素的多個副本。

建立相同元素列表範例

# 正確範例
four_nones = [None] * 4
print(four_nones)  # [None, None, None, None]

# 不正確範例(使用可變物件)
four_lists = [[]] * 4
four_lists[0].append("Ni")
print(four_lists)  # [['Ni'], ['Ni'], ['Ni'], ['Ni']]

# 正確範例(使用列表推導式)
four_lists = [[] for __ in range(4)]
four_lists[0].append("Ni")
print(four_lists)  # [['Ni'], [], [], []]

內容解密:

  1. 使用*運算元建立包含不可變元素的列表是有效的。
  2. 當處理可變物件(如列表)時,應使用列表推導式來避免意外結果。
  3. 這種技術確保了每個元素都是獨立的例項。

字串拼接

使用str.join()方法能夠高效地拼接字串。

字串拼接範例

letters = ['s', 'p', 'a', 'm']
word = ''.join(letters)
print(word)  # spam

內容解密:

  1. str.join()方法適用於列表和元組。
  2. 這種方法比使用+運算元拼接字串更高效。
  3. 特別是在處理大量字串時,效能優勢更為明顯。

使用集合(Set)提升查詢效率

在需要頻繁查詢元素的情況下,使用集合(Set)能夠顯著提升查詢效率。

集合查詢範例

x = list(('foo', 'foo', 'bar', 'baz'))
y = set(('foo', 'foo', 'bar', 'baz'))

print('foo' in x)  # True
print('foo' in y)  # True

內容解密:

  1. 集合(Set)是根據雜湊表實作的,因此查詢效率遠高於列表。
  2. 在處理大量資料時,使用集合能夠顯著提升效能。
  3. 需要注意的是,集合中的元素必須是可雜湊的(hashable)。

Python 程式設計常見陷阱與最佳實踐

例外安全上下文(Exception-safe contexts)

在處理可能引發例外(exceptions)的資源(如檔案或執行緒鎖)時,經常使用 try/finally 子句來管理這些資源。Python 2.5 之後引入了 with 陳述式和上下文管理器(context manager)協定,使得程式碼更具可讀性。

使用 with 陳述式

import threading

some_lock = threading.Lock()

with some_lock:
    # 執行某些操作
    print("Look at me: I design coastlines.\nI got an award for Norway.")

等同於傳統的 try/finally 方法

import threading

some_lock = threading.Lock()

some_lock.acquire()
try:
    # 執行某些操作
    print("Look at me: I design coastlines.\nI got an award for Norway.")
finally:
    some_lock.release()

使用 contextlib 模組

contextlib 模組提供了額外的工具,可以將函式轉換為上下文管理器,強制呼叫物件的 close() 方法,抑制例外(Python 3.4 及以上),並重新導向標準輸出和錯誤串流(Python 3.4 或 3.5 及以上)。

使用 contextlib.closing()

from contextlib import closing

with closing(open("outfile.txt", "w")) as output:
    output.write("Well, he's...he's, ah...probably pining for the fjords.")

直接使用 with 陳述式開啟檔案

由於檔案 I/O 處理物件已定義了 __enter__()__exit__() 方法,因此可以直接使用 with 陳述式開啟檔案:

with open("outfile.txt", "w") as output:
    output.write("PININ' for the FJORDS?!?!?!? What kind of talk is that?, look, why did he fall flat on his back the moment I got 'im home?\n")

內容解密:

  1. with 陳述式 自動管理資源,無需手動呼叫 close() 方法關閉檔案。
  2. contextlib.closing() 用於將物件轉換為上下文管理器,確保資源被正確釋放。
  3. __enter__()__exit__() 方法 定義了上下文管理器的行為,使得物件能夠與 with 陳述式一起使用。

常見陷阱(Common Gotchas)

Python 大多數情況下是一個乾淨且一致的語言,但仍有一些令人困惑的情況。

可變預設引數(Mutable default arguments)

在函式定義中使用可變預設引數可能導致非預期的結果。

錯誤範例

def append_to(element, to=[]):
    to.append(element)
    return to

my_list = append_to(12)
print(my_list)  # [12]

my_other_list = append_to(42)
print(my_other_list)  # [12, 42],非預期的結果

正確做法

def append_to(element, to=None):
    if to is None:
        to = []
    to.append(element)
    return to

my_list = append_to(12)
print(my_list)  # [12]

my_other_list = append_to(42)
print(my_other_list)  # [42]

內容解密:

  1. 預設引數只會在函式定義時評估一次,因此使用可變物件作為預設引數會導致非預期的行為。
  2. 使用不可變物件(如 None)作為預設引數,並在函式內部檢查是否需要初始化可變物件。

延遲繫結閉包(Late binding closures)

Python 的閉包採用延遲繫結,這意味著閉包內使用的變數值在呼叫閉包時才會被查詢。

錯誤範例

def create_multipliers():
    return [lambda x: i * x for i in range(5)]

for multiplier in create_multipliers():
    print(multiplier(2), end=" ... ")
# 輸出:8 ... 8 ... 8 ... 8 ... 8 ...

正確做法

def create_multipliers():
    return [lambda x, i=i: i * x for i in range(5)]

for multiplier in create_multipliers():
    print(multiplier(2), end=" ... ")
# 輸出:0 ... 2 ... 4 ... 6 ... 8 ...

內容解密:

  1. 延遲繫結導致所有閉包共用相同的變數,因此需要使用預設引數來實作立即繫結。
  2. 使用 functools.partial() 也能達到相同的效果,建立具有不同引數的函式。

結構化你的專案

結構化專案是指標對專案目標,做出最佳的設計決策,以充分利用Python的特性,創造出乾淨、有效的程式碼。實際而言,這意味著在程式碼和檔案/資料夾結構中,邏輯和依賴關係都必須清晰明確。

哪些函式應該放在哪些模組中?資料如何在專案中流動?哪些功能和函式可以被分組並隔離?透過回答這些問題,你可以開始從宏觀角度規劃你的最終產品。

Python Cookbook中有專門討論模組和套件的章節,詳細描述了__import__陳述式和套件的工作原理。本文旨在概述Python模組和匯入系統中,對於專案結構至關重要的部分。接著,我們將討論如何建構可擴充套件且可靠測試的程式碼。

由於Python處理匯入和模組的方式相對簡單,因此結構化Python專案相對容易。匯入模組的模型易於理解,因此你只需專注於設計專案的不同部分及其互動。

模組

模組是Python的主要抽象層之一,也是最自然的抽象層。抽象層允許程式設計師將程式碼分成包含相關資料和功能的部分。

例如,如果專案的一個層負責處理使用者操作,而另一個層負責底層資料操作,那麼最自然的方式是將所有介面功能歸類別在一個檔案中,將所有底層操作歸類別在另一個檔案中。這樣就形成了兩個獨立的模組。介面檔案會使用import modulefrom module import attribute陳述式匯入底層檔案。

只要使用import陳述式,就是在使用模組。這些模組可以是內建模組(如ossys),也可以是安裝在環境中的第三方套件(如Requests或NumPy),或是專案內部的模組。

以下程式碼展示了一些匯入陳述式的範例,並確認匯入的模組是一個具有自身資料型別的Python物件:

>>> import sys # 內建模組
>>> import matplotlib.pyplot as plt # 第三方模組
>>> import mymodule as mod # 專案內部模組
>>> print(type(sys), type(plt), type(mod))
<class 'module'> <class 'module'> <class 'module'>

內容解密:

  1. import sys:匯入內建的sys模組,用於存取與Python直譯器相關的功能。
  2. import matplotlib.pyplot as plt:匯入第三方函式庫matplotlib.pyplot,並將其別名設為plt,常用於繪圖。
  3. import mymodule as mod:匯入自定義的mymodule模組,並將其別名設為mod,便於使用。
  4. print(type(sys), type(plt), type(mod)):輸出匯入模組的型別,均為<class 'module'>,表明它們都是模組物件。

為了符合風格,請保持模組名稱簡短且小寫,並避免使用特殊符號,如點(.)或問號(?),因為這些會干擾Python尋找模組的方式。因此,像my.spam.py這樣的檔名應避免使用;Python會預期在名為my的資料夾中找到spam.py檔案,但事實並非如此。Python檔案提供了更多關於使用點符號的細節。

匯入模組

除了一些命名限制之外,使用Python檔案作為模組並無特殊要求,但瞭解匯入機制有所幫助。首先,import modu陳述式會在呼叫者的相同目錄中尋找名為modu.py的檔案定義。如果存在該檔案,則會執行它。如果找不到,Python直譯器會遞迴地在Python的搜尋路徑中尋找modu.py,如果仍未找到,則會引發ImportError例外。搜尋路徑的值取決於平台,並且包括環境中使用者或系統定義的任何目錄,在環境變數 $PYTHONPATH(或Windows中的 %PYTHONPATH%)中。可以透過Python工作階段進行操作或檢查:

import sys
>>> sys.path
[ '', '/current/absolute/path', 'etc']
# 實際列表包含所有搜尋路徑
# 當你匯入函式庫到Python時,它們將被依序搜尋。

一旦找到 modu.py,Python直譯器就會在隔離的作用域中執行該模組。 modu.py 中的任何頂層陳述式都將被執行,包括其他匯入(如果有的話)。函式和類別定義儲存在模組的字典中。

最終,模組的變數、函式和類別將透過模組的名稱空間對呼叫者可用。名稱空間是程式設計中的一個核心概念,在Python中尤其有用且強大。名稱空間提供了一個包含命名屬性的作用域,這些屬性彼此可見,但不能直接在名稱空間外部存取。

在許多語言中,包含檔案指令會導致前置處理器有效地將包含檔案的內容複製到呼叫者的程式碼中。但在Python中,被包含的程式碼被隔離在模組名稱空間中。 import modu 陳述式的結果將是在全網域名稱空間中建立一個名為 modu 的模組物件。

專案結構與程式碼組織的最佳實踐

在Python開發中,良好的專案結構與程式碼組織是確保程式碼可讀性、可維護性和可擴充套件性的關鍵。本章節將探討如何有效地組織Python專案,包括模組(Module)、套件(Package)的使用,以及如何避免常見的結構性問題。

模組與名稱空間

Python的模組機制允許開發者將相關的程式碼組織在單一檔案中,並透過import陳述式在其他模組中使用。正確使用模組和名稱空間可以避免命名衝突,並提高程式碼的可讀性。

使用 import 陳述式的最佳實踐

# 不推薦:使用 from modu import *
# 這種方式會引入模組中的所有物件,可能導致命名衝突
x = sqrt(4)

# 推薦:明確指定匯入的物件
from modu import sqrt
x = sqrt(4)

# 最佳實踐:使用模組名稱匯入
import modu
x = modu.sqrt(4)

名稱空間檢測工具

Python提供了多個內建函式來幫助檢測名稱空間,包括:

  • dir(object):傳回物件可存取的屬性列表。
  • globals():傳回當前全網域名稱空間中的屬性及其值。
  • locals():傳回當前區域性名稱空間中的屬性及其值。
import math

# 使用dir()檢視math模組的屬性
print(dir(math))

# 使用globals()檢視全網域名稱空間
print(globals())

# 使用locals()檢視區域性名稱空間
def my_function():
    local_var = 10
    print(locals())

my_function()

專案結構關鍵要素

良好的專案結構可以避免多重迴圈依賴、隱藏耦合、過度使用全域狀態等問題。

避免迴圈依賴

迴圈依賴是指兩個或多個模組相互依賴,這會導致程式無法正常執行。例如:

# furn.py
from workers import Carpenter

class Table:
    def is_done_by(self, carpenter):
        return isinstance(carpenter, Carpenter)

# workers.py
from furn import Table

class Carpenter:
    def what_do(self):
        return Table()

解決迴圈依賴的方法包括重構程式碼、使用依賴注入等。

避免隱藏耦合

隱藏耦合是指不同模組之間的依賴關係不明確,這會導致維護困難。例如,當Table類別的實作變更時,Carpenter類別的測試可能會失敗。

# 不推薦:Table和Carpenter之間存在隱藏耦合
class Carpenter:
    def work_on_table(self, table):
        # 對Table的實作有隱含假設
        table.modify_something()

# 推薦:明確定義介面,避免隱藏耦合
class TableInterface:
    def modify_something(self):
        pass

class Carpenter:
    def work_on_table(self, table: TableInterface):
        table.modify_something()

避免過度使用全域狀態

過度使用全域狀態會導致程式碼難以理解和維護。應該透過明確的引數傳遞來分享資料。

# 不推薦:使用全域變數
global_table_size = (100, 50)

class Table:
    def __init__(self):
        self.size = global_table_size

# 推薦:透過引數傳遞資料
class Table:
    def __init__(self, size):
        self.size = size

table = Table((100, 50))

套件的使用

Python的套件機制允許開發者將多個模組組織在目錄中,並透過__init__.py檔案定義套件的介面。

建立套件

mypackage/
    __init__.py
    module1.py
    module2.py

__init__.py中,可以定義套件的公開介面:

# mypackage/__init__.py
from .module1 import func1
from .module2 import func2

__all__ = ['func1', 'func2']

這樣,使用者可以方便地匯入套件中的功能:

from mypackage import func1, func2

__init__.py的最佳實踐

  • 保持__init__.py簡潔,只包含必要的匯入和定義。
  • 使用__all__變數明確指定公開的介面。