本文 AI 產出,尚未審核

Python 課程 – 函式(Functions)

主題:函式註解(docstring)


簡介

在 Python 中,函式是程式的基本組件,負責把邏輯切割成可重複使用的單位。而要讓這些函式在團隊合作、文件生成或除錯時保持可讀性與可維護性,**函式註解(docstring)**是不可或缺的工具。
docstring 不只是簡單的說明文字,它會被 Python 直譯器保存於函式物件的 __doc__ 屬性中,讓 IDE、help()、Sphinx 等文件產生工具都能自動取得說明內容。對於 初學者 來說,寫好 docstring 能培養良好的程式碼風格;對 中級開發者,則是建立大型專案、API 文件的基礎。

本篇文章將從 docstring 的語法常見寫法實務範例,一路探討到 常見陷阱 以及 最佳實踐,最後提供 實際應用場景,幫助你在寫程式時就能自然而然地加入完整且易讀的說明。


核心概念

1. 何謂 docstring?

docstring(documentation string)是放在函式、類別或模組最前面的 字串常值,使用三個單引號(''')或三個雙引號(""")包住。它會在程式執行時被 Python 讀取,並存入物件的 __doc__ 屬性。

def add(a, b):
    """回傳 a 與 b 的和。"""
    return a + b

print(add.__doc__)   # 輸出: 回傳 a 與 b 的和。

注意:docstring 必須是函式的第一條語句,否則 Python 不會將它視為說明文字。

2. 常見的 docstring 風格

風格 特色 典型使用情境
PEP 257 官方推薦的簡潔寫法,適合小型專案 基礎教學、腳本
Google style 標題式分段,易於閱讀,支援自動化工具(如 Sphinx + napoleon) 中大型專案、API 文件
NumPy/SciPy style 強調參數、回傳值與範例的詳細說明,常見於科學計算套件 數值或資料科學套件

以下示範 Google styleNumPy style 的差異:

def divide(dividend, divisor):
    """執行除法運算,並回傳結果。

    Args:
        dividend (float): 被除數。
        divisor (float): 除數,若為 0 會拋出 ZeroDivisionError。

    Returns:
        float: 除法結果。

    Raises:
        ZeroDivisionError: 當 divisor 為 0 時。
    """
    if divisor == 0:
        raise ZeroDivisionError("除數不可為 0")
    return dividend / divisor
def divide(dividend, divisor):
    """
    執行除法運算,並回傳結果。

    Parameters
    ----------
    dividend : float
        被除數。
    divisor : float
        除數,若為 0 會拋出 ZeroDivisionError。

    Returns
    -------
    float
        除法結果。

    Raises
    ------
    ZeroDivisionError
        當 divisor 為 0 時。
    """
    if divisor == 0:
        raise ZeroDivisionError("除數不可為 0")
    return dividend / divisor

3. 為什麼要寫 docstring?

  1. 自動化文件產生pydoc, Sphinx, MkDocs 等工具會直接解析 __doc__,產出 HTML、PDF 等文件。
  2. IDE 支援:大多數 IDE(如 VS Code、PyCharm)會在呼叫函式時顯示 docstring,減少切換文件查找說明的時間。
  3. 除錯與測試help()inspect.getdoc() 能即時檢視說明,對於 REPL 開發非常有幫助。
  4. 團隊協作:統一的說明格式讓新成員快速了解函式的行為、參數型別與例外情形。

程式碼範例

以下提供 5 個實用範例,示範不同情境下的 docstring 寫法與說明。

1️⃣ 基本範例 – 單行說明

def greet(name):
    """回傳問候語句,例如:'Hello, Alice!'。"""
    return f"Hello, {name}!"
  • 說明:只需要一行簡短描述時,直接放在第一行即可。
  • 適用:小工具、一次性腳本。

2️⃣ 多行說明 – 包含參數與回傳值

def factorial(n):
    """計算正整數 n 的階乘。

    Args:
        n (int): 必須是非負整數。

    Returns:
        int: n 的階乘值。

    Raises:
        ValueError: 當 n 為負數時拋出。
    """
    if n < 0:
        raise ValueError("n 必須是非負整數")
    result = 1
    for i in range(2, n + 1):
        result *= i
    return result
  • 說明:使用 Google style,清楚列出 ArgsReturnsRaises
  • 適用:函式邏輯較複雜、需要說明例外。

3️⃣ 範例程式碼 – 讓使用者快速上手

def is_palindrome(s):
    """判斷字串 s 是否為回文。

    會忽略大小寫與非字母數字字符。

    Args:
        s (str): 任意字串。

    Returns:
        bool: 若 s 為回文回傳 True,否則 False。

    Example:
        >>> is_palindrome("A man, a plan, a canal: Panama")
        True
    """
    import re
    cleaned = re.sub(r'[\W_]', '', s).lower()
    return cleaned == cleaned[::-1]
  • 說明:加入 Example 區段,讓 doctest 能直接驗證。
  • 適用:演算法或需示範呼叫方式的函式。

4️⃣ 多重回傳值與型別說明(NumPy style)

def split_name(full_name):
    """
    將全名切分為姓與名。

    Parameters
    ----------
    full_name : str
        形如 "姓 名" 或 "姓 中間名 名" 的字串。

    Returns
    -------
    tuple
        (family_name, given_name) 兩個字串。

    Notes
    -----
    若字串中僅有一個空格,視為「姓」與「名」;若有多個空格,
    只保留第一個空格前的文字為姓,其餘全部視為名。
    """
    parts = full_name.strip().split()
    if len(parts) == 1:
        return parts[0], ''
    family = parts[0]
    given = ' '.join(parts[1:])
    return family, given
  • 說明:使用 NumPy style,適合需要「Notes」或「See Also」等額外說明的情況。
  • 適用:資料處理、科學計算相關函式。

5️⃣ 裝飾器(Decorator)中的 docstring 繼承

import functools

def preserve_docstring(func):
    """裝飾器:保留被裝飾函式的 docstring。"""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        """wrapper 只負責呼叫原始函式並回傳結果。"""
        return func(*args, **kwargs)
    return wrapper

@preserve_docstring
def multiply(a, b):
    """回傳 a 與 b 的乘積。"""
    return a * b

print(multiply.__doc__)   # 輸出: 回傳 a 與 b 的乘積。
  • 說明:使用 functools.wraps 能自動把原函式的 __doc__ 複製到 wrapper,避免文件遺失。
  • 適用:開發框架或共用裝飾器時,務必保留原始說明。

常見陷阱與最佳實踐

陷阱 說明 解決方式
未將 docstring 放在第一行 Python 只會把緊接在 def 後的第一個字串視為 docstring。 確保 """說明""" 緊貼在函式定義之後,且不要在前面寫任何其他語句(包括註解)。
使用單行字串卻未加三引號 單行字串若使用單引號 '說明',會被視為普通字面值,不會存入 __doc__ 使用三引號 """說明"""'''說明'''
docstring 與程式碼行寬不一致 長段落若直接寫在一行,會超過 79 個字符,違反 PEP 8。 換行分段,或使用自動換行工具(如 black)保持 72 個字符寬度。
忽略例外說明 呼叫者可能不知道哪些例外會被拋出,導致錯誤處理不完整。 在 docstring 中加入 RaisesExceptions 區段。
裝飾器遺失原始 docstring 裝飾器未使用 functools.wraps 時,__doc__ 會被裝飾器的說明取代。 一律在自訂裝飾器內使用 @functools.wraps(original_func)
混用不同風格 同一專案內部風格不統一,會影響自動文件產生的結果。 先訂下團隊共識(PEP 257、Google、NumPy),使用 linters(如 pylintflake8-docstrings)檢查。

最佳實踐總結

  1. 統一風格:依團隊或專案選擇 PEP 257GoogleNumPy,並在 pyproject.toml 中設定 linter。
  2. 完整描述:至少包含 說明、參數、回傳值,如有例外或特殊行為則補上 RaisesNotes
  3. 使用 functools.wraps:自訂裝飾器時一定要保留原始 docstring。
  4. 加入範例(Example):讓使用者能直接看到呼叫方式,並方便 doctest 測試。
  5. 保持可讀性:每行不超過 72 個字符,段落之間空一行,使用縮排與標題提升結構清晰度。

實際應用場景

1. 企業內部 API 文件

公司內部提供的 Python SDK(例如資料庫、雲端服務)需要自動產生線上文件。只要每個公開函式遵守 Google style,使用 Sphinx + Napoleon 擴充即可在 CI 流程中自動產出 HTML 手冊,開發者只要點擊 help(module.function) 或在 IDE 中懸停即可看到完整說明。

2. 開源套件的貢獻流程

在開源專案(例如 pandasrequests)中,貢獻者提交 Pull Request 時,CI 會跑 flake8-docstrings 檢查 docstring 是否缺失或不符合規範。若缺少說明或未列出例外,PR 會被拒絕,確保文件與程式碼同步。

3. 教學或科研程式碼

在 Jupyter Notebook 中,使用 ?function_name 直接呼叫 docstring,能即時顯示說明,對於教學或科研筆記非常便利。加入 Example 區段後,doctest 甚至可以自動驗證示範程式碼是否仍然正確。

4. 自動化測試與偵錯

測試框架(如 pytest)結合 pytest-doctest,會自動執行 docstring 中的範例,確保說明與實作保持一致。這對於函式的邊界條件(例如除以 0)特別有幫助。


總結

函式註解(docstring) 不只是寫給別人的說明,更是 程式碼自我描述、文件自動化、除錯與測試 的核心資產。從 單行簡述多行參數說明範例程式碼裝飾器的繼承,掌握正確的語法與風格,能讓你的 Python 專案在團隊合作與開源貢獻時保持高可讀性與可維護性。

記住

  1. 第一行必須是 docstring,使用三引號包住。
  2. 選擇並統一風格(PEP 257 / Google / NumPy)。
  3. 完整描述參數、回傳值與可能的例外。
  4. 加入範例,讓使用者一眼就能看懂如何呼叫。
  5. 使用 functools.wraps 保留裝飾器的說明。

只要在寫每一個函式時都養成加入清晰 docstring 的習慣,未來的除錯、文件產生與團隊協作都會變得更加順暢。祝你在 Python 的旅程中,寫出既功能完整說明清楚的程式碼!