Python 專案規模擴大時,程式碼的組織和結構變得至關重要。套件機制提供了一種有效的方法,將相關模組組織成階層式的名稱空間,促程式式碼重用,避免命名衝突,並建立穩定的 API 介面。套件本質上是一個包含 __init__.py 檔案的目錄,這個檔案可以是空的,也可以包含初始化程式碼,而目錄中的其他 Python 檔案則成為套件的子模組。使用套件,我們可以透過 import package.module 或 from package import module 的方式來存取子模組。當不同套件中存在同名模組時,使用 as 語法為匯入的模組重新命名,例如 import package_a.module as module_a,可以有效避免命名衝突。
套件除了組織程式碼外,更重要的功能是建立穩定的 API 介面。在開發公開套件時,我們希望確保介面在版本迭代中保持穩定。透過 __all__ 屬性,可以精確控制公開 API 的內容,將內部實作細節隱藏起來。在套件的 __init__.py 檔案中,可以透過 from .module import * 匯入子模組的公開成員,並將它們增加到 __all__ 列表中。如此一來,當使用者使用 from package import * 時,就只會匯入 __all__ 中列出的成員。
例外處理也是 API 設計中不可或缺的一環。定義一個根異常類別,讓套件內的所有異常都繼承自它,可以幫助 API 使用者更方便地處理異常。使用者可以捕捉根異常,從而隔離 API 的錯誤,避免影響主程式碼的執行。同時,根異常也能幫助開發者更容易發現 API 模組中的錯誤。在根異常下,還可以定義更具體的異常型別,例如 ValueError、TypeError 等,以便更精確地描述錯誤情況。透過良好的例外處理機制,可以提升 API 的健壯性和易用性。
玄貓解說:Python 風格,撰寫專業級檔案與組織你的程式碼
在軟體開發的世界中,程式碼的可讀性和可維護性至關重要。玄貓認為,清晰的檔案是讓你的程式碼更易於理解和使用的關鍵。本文將探討如何撰寫 Python 模組、類別和函式的檔案,並介紹如何使用套件來組織你的程式碼,讓你的專案更具結構性和可擴充套件性。
模組檔案:讓你的程式函式庫一目瞭然
每個 Python 模組都應該有一個模組層級的檔案字串(docstring)。玄貓建議,這個檔案字串應該簡潔地描述模組的目的和功能。你可以進一步提供模組中重要類別和函式的概述,以及任何使用者需要知道的重要資訊。
"""
這個模組提供了一系列用於處理使用者帳戶的函式和類別。
主要功能包括:
- 建立、更新和刪除使用者帳戶。
- 驗證使用者身份。
- 管理使用者許可權。
重要類別:
- User:代表一個使用者帳戶。
- Permission:代表一個使用者許可權。
重要函式:
- create_user:建立一個新的使用者帳戶。
- verify_password:驗證使用者密碼。
"""
類別檔案:揭示物件的行為與屬性
每個類別都應該有一個類別層級的檔案字串。玄貓建議,第一行應該簡潔地描述類別的目的。後續的段落可以討論類別運作的重要細節。重要的是,要突出顯示類別的重要公開屬性和方法。此外,還應該為子類別提供關於如何正確與受保護屬性以及超類別方法互動的指導。
class Player(object):
"""
代表遊戲中的一個玩家。
子類別可以覆寫 `tick` 方法,以根據玩家的能力等級提供自訂動畫。
公開屬性:
- power: 未使用的能力增強道具(0 到 1 之間的浮點數)。
- coins: 在關卡中找到的硬幣數量(整數)。
"""
def __init__(self, power=0.0, coins=0):
self.power = power
self.coins = coins
def tick(self):
"""
更新玩家狀態。
"""
pass
函式檔案:清晰描述功能、引數與回傳值
每個公開函式和方法都應該有一個檔案字串。玄貓建議,第一行應該簡潔地描述函式的功能。後續的段落應該描述任何特定的行為以及函式的引數。任何回傳值都應該被提及。任何呼叫者必須處理的例外情況也應該被解釋。
def find_anagrams(word: str, dictionary: set) -> list:
"""
尋找一個單字的所有變位詞。
這個函式的執行速度取決於 `dictionary` 容器中成員資格測試的速度。
如果字典是一個列表,它將會很慢;如果它是一個集合,它將會很快。
Args:
word: 目標單字的字串。
dictionary: 包含所有已知為實際單字的字串的容器。
Returns:
找到的變位詞列表。如果沒有找到,則為空列表。
"""
anagrams = []
sorted_word = sorted(word)
for candidate in dictionary:
if sorted(candidate) == sorted_word and candidate != word:
anagrams.append(candidate)
return anagrams
玄貓提醒,在編寫函式的檔案字串時,還有一些特殊情況需要注意:
- 如果你的函式沒有引數和一個簡單的回傳值,一個單一句子的描述可能就足夠了。
- 如果你的函式沒有回傳任何東西,最好省略任何關於回傳值的提及,而不是說「回傳 None」。
- 如果你不期望你的函式在正常操作期間引發例外,不要提及這個事實。
- 如果你的函式接受可變數量的引數(
*args)或關鍵字引數(**kwargs),在檔案化的引數列表中使用*args和**kwargs來描述它們的目的。 - 如果你的函式有帶有預設值的引數,這些預設值應該被提及。
- 如果你的函式是一個產生器,那麼你的檔案字串應該描述當它被迭代時產生器會產生什麼。
- 如果你的函式是一個協程,那麼你的檔案字串應該包含協程產生什麼,它期望從 yield 表示式接收什麼,以及它何時會停止迭代。
使用套件組織模組:開發穩定的 API
隨著程式碼函式庫的增長,重新組織其結構是很自然的。你會將較大的函式拆分為較小的函式。你會將資料結構重構為輔助類別。你會將功能分離到相互依賴的各種模組中。在某個時候,你會發現自己有太多的模組,以至於你需要程式中的另一個層級來使其易於理解。為此,Python 提供了套件。套件是包含其他模組的模組。
在大多數情況下,套件是透過將一個名為 __init__.py 的空檔案放入一個目錄中來定義的。一旦 __init__.py 存在,該目錄中的任何其他 Python 檔案都可以使用相對於該目錄的路徑匯入。例如,假設你的程式中有以下目錄結構。
main.py
mypackage/__init__.py
mypackage/models.py
mypackage/utils.py
要匯入 utils 模組,你可以使用包含套件目錄名稱的絕對模組名稱。
# main.py
from mypackage import utils
當你在其他套件中存在套件目錄時,這個模式會繼續(例如 mypackage.foo.bar)。
玄貓提醒:Python 3.4 引入了名稱空間套件,這是一種更靈活的定義套件的方式。名稱空間套件可以由來自完全獨立的目錄、zip 檔案甚至遠端系統的模組組成。
套件提供的功能在 Python 程式中有兩個主要目的:
- 名稱空間: 套件的第一個用途是幫助你將模組劃分為獨立的名稱空間。這允許你擁有許多具有相同檔案名稱但具有唯一不同絕對路徑的模組。
- 穩定的 API: 套件可以幫助你建立穩定的 API。你可以將內部模組放在一個套件中,並將公開模組放在另一個套件中。這樣,你可以更改內部模組的程式碼,而不會影響到使用你的程式碼的其他人。
例如,以下程式碼從兩個具有相同名稱 utils.py 的模組匯入屬性。這是可行的,因為模組可以透過它們的絕對路徑來定址。
# main.py
from analysis.utils import log_base2_bucket
## Python 模組化設計:善用套件管理你的程式碼
在撰寫大型 Python 專案時,程式碼的組織方式至關重要。套件(Packages)提供了一種有效的方法,能夠將相關模組組織在一起,形成一個具備階層式的名稱空間。這不僅有助於程式碼的重用,還能避免命名衝突,並為外部使用者提供穩定的 API 介面。
### 套件的基礎:建立與使用
在 Python 中,一個目錄只要包含 `__init__.py` 檔案,就能被視為一個套件。這個檔案可以是空的,也可以包含套件的初始化程式碼。套件目錄下的其他 `.py` 檔案則成為套件的子模組。
舉例來說,假設我們有一個名為 `mypackage` 的套件,其目錄結構如下:
mypackage/ init.py module1.py module2.py
要使用 `module1.py` 中的函式或類別,可以使用以下 import 語法:
```python
import mypackage.module1
mypackage.module1.my_function()
from mypackage import module1
module1.my_function()
from mypackage.module1 import my_function
my_function()
名稱空間衝突的解決方案
當套件中的函式、類別或子模組有相同的名稱時,直接 import 屬性可能會導致命名衝突。例如:
# main2.py
from analysis.utils import inspect
from frontend.utils import inspect # Overwrites!
後面的 import 陳述式會覆寫前面 inspect 的值。
解決方案是使用 import ... as ... 語法,為 import 的物件重新命名:
# main3.py
from analysis.utils import inspect as analysis_inspect
from frontend.utils import inspect as frontend_inspect
value = 33
if analysis_inspect(value) == frontend_inspect(value):
print('Inspection equal!')
as 語法可以重新命名任何透過 import 陳述式引入的物件,包括整個模組。
建立穩定的 API 介面
套件的另一個重要用途是為外部使用者提供穩定的 API 介面。當你開發一個公開的套件時,你會希望確保其功能在不同版本之間不會有太大變動。為了實作這個目標,你需要對外部使用者隱藏套件內部的程式碼組織方式。這樣一來,你就可以在不影響現有使用者的情況下,重構和改進套件的內部模組。
Python 可以使用 __all__ 這個特殊屬性來限制暴露給 API 使用者的介面範圍。__all__ 的值是一個列表,包含了所有要從模組中匯出的名稱,作為其公開 API 的一部分。當使用 from foo import * 時,只會從 foo 匯入 foo.__all__ 中列出的屬性。如果 foo 中沒有 __all__,則只會匯入公開屬性(即沒有底線開頭的屬性)。
例如,假設你想提供一個套件來計算移動射彈之間的碰撞。以下定義 mypackage 的 models 模組,其中包含射彈的表示:
# models.py
__all__ = ['Projectile']
class Projectile(object):
def __init__(self, mass, velocity):
self.mass = mass
self.velocity = velocity
同時,在 mypackage 中定義一個 utils 模組,用於對 Projectile 例項執行操作,例如模擬它們之間的碰撞。
# utils.py
from .models import Projectile
__all__ = ['simulate_collision']
def _dot_product(a, b):
# ...
def simulate_collision(a, b):
# ...
現在,將所有公開 API 作為一組屬性提供在 mypackage 模組上。這將允許下游使用者始終直接從 mypackage 匯入,而不是從 mypackage.models 或 mypackage.utils 匯入。這確保了即使 mypackage 的內部組織發生變化(例如,刪除了 models.py),API 使用者的程式碼也能繼續工作。
要使用 Python 套件實作此目的,需要修改 mypackage 目錄中的 __init__.py 檔案。匯入後,此檔案實際上會變成 mypackage 模組的內容。因此,你可以透過限制匯入到 __init__.py 中的內容來為 mypackage 指定明確的 API。由於所有內部模組都已指定 __all__,因此可以透過簡單地從內部模組匯入所有內容並相應地更新 __all__ 來公開 mypackage 的公開介面。
# __init__.py
__all__ = []
from .models import *
__all__ += models.__all__
from .utils import *
__all__ += utils.__all__
以下是 API 的使用者,它直接從 mypackage 匯入,而不是存取內部模組:
# api_consumer.py
from mypackage import *
a = Projectile(1.5, 3)
b = Projectile(4, 1.7)
after_a, after_b = simulate_collision(a, b)
值得注意的是,內部專用函式(如 mypackage.utils._dot_product)在 mypackage 上對 API 使用者不可用,因為它們不存在於 __all__ 中。從 __all__ 中省略意味著它們沒有被 from mypackage import * 陳述式匯入。內部專用名稱實際上是隱藏的。
當提供明確、穩定的 API 非常重要時,整個方法效果很好。但是,如果你要建構一個 API 以在自己的模組之間使用,則 __all__ 的功能可能不必要,應避免使用。套件提供的名稱空間通常足以讓程式設計師團隊協作處理他們控制的大量程式碼,同時保持合理的介面邊界。
玄貓認為,在設計 Python 專案時,善用套件不僅能提升程式碼的可讀性和可維護性,還能為團隊協作帶來更大的便利。
為何需要定義根異常:隔離 API 呼叫者
在設計模組的 API 時,拋出的異常與定義的函式和類別同樣重要。Python 內建了一套異常體系,但有時直接使用內建異常型別來報告錯誤可能不是最佳選擇。玄貓認為,為 API 定義自己的異常層級結構更有益處。
建立模組的根異常
在模組中提供一個根 Exception,讓該模組拋出的所有其他異常都繼承自這個根異常。
# my_module.py
class Error(Exception):
"""此模組引發的所有異常的基底類別。"""
class InvalidDensityError(Error):
"""提供的密度值存在問題。"""
根異常的優勢
隔離 API 消費者
API 的使用者可以輕鬆地捕捉你特意引發的所有異常。例如:
try: weight = my_module.determine_weight(1, -1) except my_module.Error as e: logging.error('發生未預期的錯誤:%s', e)這可以防止 API 的異常向上傳播並破壞呼叫程式,從而將呼叫程式碼與 API 隔離開來。
協助發現 API 模組中的錯誤
如果程式碼僅特意引發在模組層級結構中定義的異常,那麼模組引發的所有其他型別的異常一定是未預期引發的,這些都是 API 程式碼中的錯誤。
try: weight = my_module.determine_weight(1, -1) except my_module.InvalidDensityError: weight = 0 except my_module.Error as e: logging.error('呼叫程式碼中的錯誤:%s', e) except Exception as e: logging.error('API 程式碼中的錯誤:%s', e) raiseAPI 的未來擴充性
隨著時間的推移,你可能希望擴充 API 以在某些情況下提供更具體的異常。例如,可以新增一個
Exception子類別,指示提供負密度的錯誤情況。# my_module.py class NegativeDensityError(InvalidDensityError): """提供的密度值為負數。""" def determine_weight(volume, density): if density < 0: raise NegativeDensityError呼叫程式碼將繼續像以前一樣工作,因為它已經捕捉了
InvalidDensityError異常(NegativeDensityError的父類別)。將來,呼叫者可以決定特殊處理新型別的異常並相應地更改其行為。try: weight = my_module.determine_weight(1, -1) except my_module.NegativeDensityError as e: raise ValueError('必須提供非負密度') from e except my_module.InvalidDensityError: weight = 0 except my_module.Error as e: logging.error('呼叫程式碼中的錯誤:%s', e) except Exception as e: logging.error('API 程式碼中的錯誤:%s', e) raise可以透過在根異常正下方提供更廣泛的異常集來進一步保護 API 的未來發展。例如,假設有一組與計算權重相關的錯誤,另一組與計算體積相關的錯誤,第三組與計算密度相關的錯誤。
# my_module.py class WeightError(Error): """重量計算錯誤的基底類別。""" class VolumeError(Error): """體積計算錯誤的基底類別。""" class DensityError(Error): """密度計算錯誤的基底類別。"""特定異常將繼承自這些一般異常。每個中間異常都充當其自身種類別的根異常。這使得根據廣泛的功能將呼叫程式碼層與 API 程式碼隔離開來變得更加容易。這比讓所有呼叫者捕捉一長串非常具體的
Exception子類別要好得多。
使用 __all__ 屬性控制模組 API
在 Python 中,可以使用 __all__ 這個特殊屬性來明確定義一個套件或模組的公開 API。這能有效隱藏套件內部的實作細節,只將公開的名稱匯入到套件的 __init__.py 檔案中,或者透過在內部成員名稱前加上底線來達到隱藏效果。
# my_package/__init__.py
from .module_a import PublicClassA
from .module_b import public_function_b
__all__ = ['PublicClassA', 'public_function_b']
何時使用 __all__
- 大型專案:在大型專案中,明確的 API 有助於維護程式碼的清晰度和可維護性。
- 公開套件:當你開發一個將被廣泛使用的套件時,使用
__all__可以確保使用者只依賴於你希望他們使用的部分。
何時可以省略 __all__
- 小型團隊或單一程式碼函式庫:在小型團隊或單一程式碼函式庫中,使用
__all__可能不那麼必要。
玄貓的建議
玄貓建議,在設計 API 時,應考慮使用 __all__ 來明確定義公開介面,並定義根異常來隔離 API 呼叫者,這有助於提高程式碼的可維護性和可擴充性。
總結:
- 為模組定義根異常,使 API 消費者可以隔離 API。
- 捕捉根異常可以幫助您發現使用 API 的程式碼中的錯誤。
- 捕捉 Python
Exception基底類別可以幫助您發現 API 實作中的錯誤。 - 中間根異常可讓您在將來新增更具體的異常型別,而不會破壞 API 消費者。