Home
探索 Uedu
學生控制台
註冊會員/登入
研究知情同意中心
教師控制台
課程設定
支援與訊息
Uptime 數據

UeduGPTs

--

Jupyters

4

UG26 CISOSE26
臺北 AQI 46 · 臺中 AQI 28 · 臺南 AQI 24 · 高雄 AQI 33

AI 回覆桌面通知

AI 助教回覆完成時顯示桌面通知

聊天訊息通知

同學在討論區發送訊息時通知

聲音通知

每當有新通知時播放提示音

函數與模組

Python 函數與模組:從重複貼上到優雅重用

從 def、參數與作用域,一路走到自訂模組與套件,學會用函數把程式拆成可組裝的積木。

從重複貼上到優雅重用:一個記帳小程式的進化

想像你正在寫一個記帳小工具,要把每筆金額換算成含稅價格。第一個版本你可能會這樣寫:

price1 = 100
total1 = price1 * 1.05
print(f"含稅後:{total1}")

price2 = 250
total2 = price2 * 1.05
print(f"含稅後:{total2}")
# 輸出:
# 含稅後:105.0
# 含稅後:262.5

看出問題了嗎?同樣的邏輯被複製貼上,一旦稅率從 5% 改成 6%,你得逐行修改,漏改一處就出 bug。函數(function)正是為了解決這種重複而生。把「一段做某件事的程式碼」打包起來,給它一個名字,之後就能反覆呼叫。這篇文章我們從最基本的 def 出發,一路走到模組(module)與套件(package),讓你的程式從一團散沙變成可組裝的積木。

函數與模組概念示意圖

用 def 定義你的第一個函數

在 Python 中,用 def 關鍵字定義函數。基本骨架是「def 名稱(參數):」加上縮排的函數主體:

def add_tax(price):
    return price * 1.05

print(add_tax(100))   # 輸出:105.0
print(add_tax(250))   # 輸出:262.5

現在稅率邏輯只存在一個地方。price 是參數(parameter),代表呼叫時要傳入的輸入;return 把計算結果交還給呼叫端。如果一個函數沒有寫 return,它會自動回傳 None

def greet(name):
    print(f"你好,{name}")

result = greet("小明")   # 輸出:你好,小明
print(result)            # 輸出:None

這裡要分清楚兩件事:print() 是把文字「顯示」在畫面上,return 是把值「交還」給程式繼續使用。初學者最常混淆這兩者——一個函數只負責印出東西卻沒有 return,那它的回傳值就是 None,沒辦法拿去做後續運算。

參數與回傳:函數的輸入與輸出

函數可以接收多個參數,也可以回傳多個值(其實是回傳一個元組(tuple)):

def divide(dividend, divisor):
    quotient = dividend // divisor
    remainder = dividend % divisor
    return quotient, remainder

q, r = divide(17, 5)
print(f"商:{q},餘數:{r}")
# 輸出:商:3,餘數:2

return quotient, remainder 實際上回傳了一個元組 (3, 2),左側的 q, r = ... 再把它拆開(unpacking)。這是 Python 很慣用的寫法,比起用 list 包起來再用索引取值要清楚得多。

呼叫函數時,預設是按照位置(positional)對應的——第一個值給第一個參數,第二個值給第二個參數。所以 divide(17, 5) 的 17 是被除數、5 是除數,順序不能亂。

預設值:讓參數可以省略

如果某個參數大多數時候都是同一個值,可以給它預設值(default value),呼叫時就能省略:

def add_tax(price, rate=0.05):
    return price * (1 + rate)

print(add_tax(100))        # 用預設 5%,輸出:105.0
print(add_tax(100, 0.10))  # 改成 10%,輸出:110.00000000000001

有預設值的參數必須放在沒有預設值的參數後面,否則 Python 會直接報語法錯誤:

# 反模式:會 SyntaxError
# def add_tax(rate=0.05, price):
#     ...

道理很直觀:如果可省略的參數排在前面,Python 在解析位置引數時就會無所適從。

關鍵字引數:用名字指定,順序不再重要

當參數變多,光靠位置容易記錯。這時可以用關鍵字引數(keyword argument),直接寫出參數名稱:

def make_profile(name, age, city):
    return f"{name},{age} 歲,住在 {city}"

# 位置引數:必須記順序
print(make_profile("小華", 20, "台北"))

# 關鍵字引數:順序可以打亂,意圖更清楚
print(make_profile(city="高雄", name="小美", age=22))
# 輸出:
# 小華,20 歲,住在 台北
# 小美,22 歲,住在 高雄

關鍵字引數讓呼叫端的程式碼自我說明(self-documenting)。當你看到 make_profile(city="高雄", ...),立刻知道 "高雄" 是城市。位置引數與關鍵字引數可以混用,但位置引數一定要寫在前面

print(make_profile("小強", city="台中", age=25))   # 正確
# print(make_profile(name="小強", 25, "台中"))      # 錯誤:位置引數不能跟在關鍵字引數後

args 與 *kwargs:接收不定數量的引數

有時你不知道呼叫端會傳幾個值。*args 會把多餘的位置引數收集成一個元組,**kwargs 會把多餘的關鍵字引數收集成一個字典(dict):

def total(*args):
    s = 0
    for n in args:
        s += n
    return s

print(total(1, 2, 3))         # 輸出:6
print(total(10, 20, 30, 40))  # 輸出:100
def describe(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}:{value}")

describe(name="小明", score=95, subject="數學")
# 輸出:
# name:小明
# score:95
# subject:數學

名稱 argskwargs 只是慣例,真正起作用的是 ***。你也可以把兩者跟一般參數一起用,完整的參數順序是:一般參數、*args、有預設值的參數、**kwargs

def log(level, *messages, sep=" | ", **meta):
    text = sep.join(messages)
    print(f"[{level}] {text}")
    for k, v in meta.items():
        print(f"    {k}={v}")

log("INFO", "啟動", "載入設定", user="admin", pid=1234)
# 輸出:
# [INFO] 啟動 | 載入設定
#     user=admin
#     pid=1234

反過來,*** 也能在呼叫時「拆開」序列與字典:

nums = [1, 2, 3]
print(total(*nums))   # 等同 total(1, 2, 3),輸出:6

info = {"name": "小美", "score": 88, "subject": "英文"}
describe(**info)      # 等同 describe(name="小美", ...)

作用域 LEGB:變數從哪裡來

當你在函數裡用到一個變數,Python 怎麼決定它指的是哪一個?答案是 LEGB 規則,依序搜尋四層:

  • Local:當前函數內部
  • Enclosing:外層巢狀函數
  • Global:模組層級(整個檔案)
  • Built-in:Python 內建(如 lenprint
x = "global"          # G

def outer():
    x = "enclosing"   # E
    def inner():
        x = "local"   # L
        print(x)
    inner()

outer()               # 輸出:local

inner 印出 x 時,先找自己的 Local,找到 "local" 就停止。如果把 inner 裡的賦值拿掉,它會往外找到 Enclosing 的 "enclosing";再拿掉就找到 Global 的 "global"

要特別小心:在函數內賦值會建立一個新的區域變數,而不是修改外層的同名變數。這常造成困惑:

count = 0

def increment():
    count = count + 1   # UnboundLocalError!
    return count

# increment()  # 會報錯

因為函數裡有 count = ...,Python 認定 count 是區域變數,但等號右側又要先讀取它,此時它還沒被賦值,於是出錯。如果真要修改全域變數,要明確宣告 global(修改 enclosing 變數則用 nonlocal):

count = 0

def increment():
    global count
    count += 1
    return count

print(increment())   # 輸出:1
print(increment())   # 輸出:2

不過實務上,過度依賴 global 是反模式——它讓資料流變得難以追蹤。比較好的做法是把值當參數傳入、把結果 return 出來。

動手寫一段:成績統計小工具

把前面的概念串起來,寫一個能算出任意數量分數統計的小程式:

def analyze(*scores, passing=60):
    """計算一組分數的統計資訊。

    Args:
        *scores: 任意數量的分數
        passing: 及格門檻,預設 60

    Returns:
        包含平均、最高、及格人數的字典
    """
    if not scores:
        return {"average": 0, "highest": 0, "passed": 0}

    average = sum(scores) / len(scores)
    highest = max(scores)
    passed = sum(1 for s in scores if s >= passing)
    return {
        "average": round(average, 1),
        "highest": highest,
        "passed": passed,
    }


report = analyze(85, 92, 47, 60, 73, passing=60)
print(f"平均:{report['average']}")
print(f"最高:{report['highest']}")
print(f"及格人數:{report['passed']}")
# 輸出:
# 平均:71.4
# 最高:92
# 及格人數:4

這段程式用 *scores 接收任意數量分數,用 passing 提供有預設值的及格門檻,用 dict 回傳結構化的結果,還寫了 docstring 說明用途。這正是符合 Python 慣例的函數設計。

import 與自訂模組:把函數搬出去重用

當函數越來越多,全擠在一個檔案裡會難以維護。模組(module)就是一個 .py 檔案,可以被別的檔案 import 進來使用。先看標準函式庫:

import math
print(math.sqrt(16))        # 輸出:4.0

from random import randint
print(randint(1, 6))        # 輸出:1 到 6 之間的隨機整數

import statistics as stats  # 取別名
print(stats.mean([1, 2, 3, 4]))   # 輸出:2.5

import math 把整個模組載入,用 math.sqrt 存取;from random import randint 只取出需要的名稱,可以直接寫 randintimport statistics as stats 給模組取別名,方便書寫。

自訂模組也一樣。假設你有一個 tax_utils.py

# tax_utils.py
TAX_RATE = 0.05

def add_tax(price, rate=TAX_RATE):
    return price * (1 + rate)

def remove_tax(total, rate=TAX_RATE):
    return total / (1 + rate)

在同目錄的另一個檔案就能引用它:

# main.py
import tax_utils

print(tax_utils.add_tax(100))       # 輸出:105.0
print(tax_utils.TAX_RATE)           # 輸出:0.05

from tax_utils import remove_tax
print(round(remove_tax(105), 2))    # 輸出:100.0

這裡有個重要慣例:模組底部常見的 if __name__ == "__main__": 區塊。當檔案被直接執行時,__name__ 等於 "__main__";被 import 時則等於模組名稱。藉此可以把「測試用的主程式」與「被引用的函數」分開:

# tax_utils.py 底部
if __name__ == "__main__":
    # 只有直接執行 python tax_utils.py 才會跑這段
    print("自我測試:", add_tax(200))

套件:用資料夾組織多個模組

當模組多到一個資料夾裝不下時,就用套件(package)——一個包含多個模組的資料夾。傳統上資料夾裡會放一個 __init__.py(可以是空檔),標示這是一個套件:

finance/
├── __init__.py
├── tax.py
└── interest.py

引用時用點號表示階層:

from finance.tax import add_tax
from finance import interest

print(add_tax(100))
print(interest.compound(1000, 0.02, 3))

這種階層式組織,讓大型專案的程式碼各歸其位,也是 Uedu 後端用 app_quizapp_survey 等模組拆分功能的同樣精神——每個檔案專注一件事,彼此透過 import 協作。

常見錯誤

  • 混淆 printreturn:函數只 printreturn,回傳值就是 None,無法拿去做後續運算。要把結果交給程式用,請用 return
  • 預設值參數排在前面def f(rate=0.05, price) 會直接語法錯誤。有預設值的參數一律放後面。
  • 位置引數寫在關鍵字引數後面f(name="x", 25) 會報錯。位置引數必須在前。
  • 函數內想改全域變數卻沒宣告 globalcount = count + 1 在函數裡會引發 UnboundLocalError,因為 Python 把 count 當成尚未賦值的區域變數。
  • 濫用 global:能用參數傳入、回傳值傳出,就不要碰全域變數——它讓資料流難以追蹤、測試困難。

深入探討(研究所視角)

可變預設值的陷阱

預設值有一個經典坑:預設值物件只在函數「定義時」建立一次,而非每次呼叫時重建。如果預設值是可變物件(mutable)如 list 或 dict,這個物件會在多次呼叫間被共用:

def append_item(item, bucket=[]):   # 反模式!
    bucket.append(item)
    return bucket

print(append_item(1))   # 輸出:[1]
print(append_item(2))   # 輸出:[1, 2]  —— 竟然累積了!
print(append_item(3))   # 輸出:[1, 2, 3]

那個 [] 在函數物件建立時就生成,綁在函數的 __defaults__ 屬性上,所有呼叫共用同一個 list。正確做法是用 None 當哨兵(sentinel),在函數內判斷後才建立新物件:

def append_item(item, bucket=None):
    if bucket is None:
        bucket = []
    bucket.append(item)
    return bucket

print(append_item(1))   # 輸出:[1]
print(append_item(2))   # 輸出:[2]  —— 每次都是新的

這個設計取捨源於 Python「預設值在定義期求值(evaluated at definition time)」的語義。理解它,你才能在看似詭異的 bug 面前不慌張。

第一級函數與閉包

在 Python 中,函數是第一級物件(first-class object):可以指派給變數、放進資料結構、當作引數傳遞、也能被當作回傳值。這是函數式程式設計(functional programming)的基礎:

def square(x):
    return x * x

operations = [square, abs, str]   # 函數放進 list
for op in operations:
    print(op(-3))
# 輸出:
# 9
# 3
# -3

更進一步,函數可以「記住」它定義時所在的外層環境,形成閉包(closure)。下面的 make_multiplier 回傳一個內層函數,後者捕捉了外層的 factor

def make_multiplier(factor):
    def multiplier(n):
        return n * factor   # 記住外層的 factor
    return multiplier

double = make_multiplier(2)
triple = make_multiplier(3)
print(double(10))   # 輸出:20
print(triple(10))   # 輸出:30

doubletriple 各自封存了不同的 factor 值。這些被捕捉的變數存放在函數的 __closure__ 屬性中(每個是一個 cell 物件)。閉包正是裝飾器(decorator)、回呼(callback)、惰性求值等高階模式的基石。如果內層函數需要修改被捕捉的變數,就要用前面提過的 nonlocal 宣告。

模組載入機制

import 背後並非單純「把檔案貼進來」。第一次 import 一個模組時,Python 會執行整個模組檔案(這就是為什麼模組頂層的 print 會在 import 時觸發),把產生的命名空間包成一個 module 物件,並快取到 sys.modules 字典裡。之後再次 import 同一模組,Python 直接從 sys.modules 取出快取,不會重新執行

import sys
import math
print("math" in sys.modules)   # 輸出:True

這個快取機制有兩個重要後果。其一,模組頂層程式碼具有「單例(singleton)」般的初始化效果——設定資料庫連線、載入設定檔等只會跑一次。其二,如果你在開發中改了模組原始碼,已經 import 的程式不會自動看到變更,必須重啟直譯器(或在互動環境用 importlib.reload)。

Python 搜尋模組的路徑記錄在 sys.path 串列裡,依序包含當前目錄、PYTHONPATH 環境變數指定的路徑、以及標準函式庫與第三方套件的安裝位置。理解這條搜尋鏈,就能解釋「為什麼我的 import my_module 找不到檔案」這類問題——多半是因為該檔案不在 sys.path 的任一目錄中。掌握載入機制與快取語義,是從「會寫函數」邁向「會設計模組架構」的關鍵一步。

AI 共讀助教正在陪你讀:Python 函數與模組:從重複貼上到優雅重用
嗨!我是這篇文章的共讀助教,只根據〈Python 函數與模組:從重複貼上到優雅重用〉的內容回答。可以問我「解釋某段」「舉個例子」「出題考我」,或反白文中段落後點下方「解釋選取段落」。