Home
Explore Uedu
Student Console
Register as Member/Login
(2) In future presentations of the research findings, in addition to the course project website and public presentations, your real name and personal information will not appear in this research report. If you are interested in the research results, we can provide you with an executive summary after the study is completed.
問卷中心
Teacher Console
Course Setup
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

0

Local AI

--

CISOSE26 本地 AI UG26
政治大學 AQI 18 29°C

AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

函數與模組

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 函數與模組:從重複貼上到優雅重用〉的內容回答。可以問我「解釋某段」「舉個例子」「出題考我」,或反白文中段落後點下方「解釋選取段落」。