Day 8 函式進階
引言
昨天我們認識了 Python 函式的基本功:定義函式、傳遞參數、回傳結果、加上說明字串。大部分日常的程式需求,光靠這些基礎就能應付。但當程式規模逐漸變大,你會遇到一些更靈活的需求——例如函式需要接收不確定數量的引數、想把小型邏輯直接寫成匿名函式、或者需要理解變數在不同範圍的可見性。
這篇文章會帶你認識函式的進階主題:*args 與 **kwargs 用來接收任意數量的引數、LEGB 作用域規則幫助理解變數的可見性、lambda 匿名函式適合用在需要小函式的場合,以及遞迴如何用「自己呼叫自己」的方式拆解問題。這些技巧可以讓你寫出更具彈性的函式,也為日後學習函式型程式設計與深度學習鋪路。
*args:接收任意數量的位置引數
當你不確定函式會被傳入幾個引數時,可以用 *args 來打包所有額外的位置引數。args 在函式內會被當成「元組(tuple)」處理,你可以像操作一般元組一樣走訪它。
def add_all(*args):
print(f"收到的引數:{args}")
return sum(args)
print(add_all(1, 2, 3)) # 輸出 6,args = (1, 2, 3)
print(add_all(10, 20, 30, 40)) # 輸出 100,args = (10, 20, 30, 40)
星號 * 是關鍵,它告訴 Python「把剩下的引數全部打包起來」。變數名稱不一定要叫 args,例如 *numbers 也可以,但 *args 是社群慣例,讀到的人會立刻理解這個變數的用途。
**kwargs:接收任意數量的關鍵字引數
如果想把關鍵字引數(name=value)也一起打包,可以用 **kwargs。kwargs 在函式內會是「字典(dict)」,鍵是參數名稱,值是對應的數值。
def print_info(**kwargs):
for key, value in kwargs.items():
print(f"{key}: {value}")
print_info(name="Alice", age=25, city="Taipei")
執行程式會輸出 name: Alice、age: 25、city: Taipei。*args 與 **kwargs 也可以同時使用,但順序必須是「位置參數 → *args → 關鍵字參數 → **kwargs」。這個語法在設計需要高度彈性的 API(例如裝飾器、框架的設定傳遞)時非常有用。
作用域:LEGB 規則
昨天的內容我們簡單提過變數有「區域」與「全域」的差別,實際上 Python 用一套稱為 LEGB 的規則來決定變數的可見範圍,由內到外依序為:Local(函式內)、Enclosing(外層函式)、Global(模組層級)、Built-in(Python 內建)。
x = "全域"
def outer():
x = "外層函式"
def inner():
x = "內層函式"
print(x) # 輸出「內層函式」
inner()
print(x) # 輸出「外層函式」
outer()
print(x) # 輸出「全域」
同名的變數在不同的範圍可以各自獨立,這就是 LEGB 規則的作用。實務上,常見的作法是「避免與內建名稱衝突」,例如不要把自己的變數取名叫 list、dict、str 等,否則可能會覆蓋掉內建功能。除非真的需要修改全域變數,否則盡量讓函式透過參數與 return 與外界溝通,這樣可以避免很多難以追蹤的 bug。
lambda 匿名函式
有些函式邏輯非常簡單,只有一兩行,卻要為了它特地寫一個 def 區塊,這時可以用 lambda 表達式以做為替代。lambda 是一種匿名函式(anonymous function),語法是 lambda 參數: 運算式,會自動回傳運算式的結果。
# 一般寫法
def square(x):
return x ** 2
# lambda 寫法
square = lambda x: x ** 2
print(square(5)) # 輸出 25
# 常見搭配:sorted() 用 lambda 指定排序鍵
students = [("Alice", 85), ("Bob", 92), ("Carol", 78)]
students_sorted = sorted(students, key=lambda s: s[1])
print(students_sorted) # [('Carol', 78), ('Alice', 85), ('Bob', 92)]
lambda 適合用在「邏輯簡單、只用一次」的場景,例如搭配 sorted()、map()、filter() 等內建函式使用。但若邏輯較複雜、需要多行程式碼或註解,仍然建議使用一般的 def 函式,可讀性會好很多。
遞迴:自己呼叫自己
遞迴(recursion)是一種特殊的技巧:函式在內部「呼叫自己」。它特別適合用於「問題可以拆成性質相同但規模更小的子問題」的情境,例如階乘、費氏數列、樹狀結構走訪等。
# 計算 n 的階乘:n! = n * (n-1) * ... * 1
def factorial(n):
if n == 0 or n == 1:
return 1
return n * factorial(n - 1)
print(factorial(5)) # 輸出 120
遞迴函式一定要有「終止條件」,否則會無限呼叫自己直到超出最大遞迴深度而報錯。上例的 if n == 0 or n == 1: return 1 就是終止條件。除了階乘,遞迴也能用來實作費氏數列:
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print([fibonacci(i) for i in range(10)])
# 輸出 [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
遞迴程式碼簡潔優雅,但對大型輸入可能會有效能或遞迴深度問題(Python 預設遞迴深度約 1000 層)。實務上若需要處理大量資料,通常會改用迴圈或加入「記憶化(memoization)」快取重複計算的結果。
常見錯誤與除錯建議
寫函式時,有幾個常見的錯誤值得留意。熟悉這些坑點能讓你在除錯時更快找到問題所在。
忘記 return:如果函式內沒有寫 return,預設會回傳 None,呼叫端的結果就會是 None 而不是預期的值。
預設參數使用串列或字典:Python 的預設參數是在「函式定義時」就建立的,因此每次呼叫共用同一個物件,容易踩到 mutable default argument 的陷阱。建議改用 None 當預設值,函式內再建立新物件。
# 錯誤示範
def add_item(item, items=[]):
items.append(item)
return items
# 正確示範
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
遞迴沒寫終止條件:忘了寫終止條件會導致 RecursionError。寫遞迴時,建議先把「最小問題」的答案寫在最前面,再處理一般情況。
函式當作參數傳遞
在 Python 中,函式本身也是一個「物件」,可以當作參數傳給別的函式,也能從函式回傳另一個函式。這種「函式優先級(first-class function)」的特性,是函式型程式設計的基礎,也是 map()、filter() 等內建函式的運作原理。
def apply_twice(func, value):
return func(func(value))
def double(x):
return x * 2
print(apply_twice(double, 3)) # 輸出 12(先 double(3)=6,再 double(6)=12)
這種把行為當參數傳遞的做法,能讓程式碼更靈活。例如同一個排序函式,可以透過傳入不同的比較函式來改變排序規則;同一個篩選函式,可以透過傳入不同的條件函式來過濾不同資料。在學習 Pandas、scikit-learn 等套件時,你也會大量看到這種「把函式傳給函式」的寫法。
裝飾器初探
「函式可以當參數、也可以被函式回傳」這兩個特性組合起來,就能寫出「裝飾器(decorator)」。裝飾器是一種「接收函式、回傳新函式」的函式,常用來在不修改原本程式碼的前提下,額外加上記錄、除錯、權限檢查等功能。
def logger(func):
def wrapper(*args, **kwargs):
print(f"呼叫 {func.__name__},引數:{args}, {kwargs}")
result = func(*args, **kwargs)
print(f"回傳值:{result}")
return result
return wrapper
@logger
def add(a, b):
return a + b
print(add(3, 5))
執行後會看到三行輸出:「呼叫 add」、「回傳值:8」,以及最後的「8」。@logger 是裝飾器的語法糖,等同於 add = logger(add)。裝飾器在網頁框架、測試工具與深度學習框架中都非常常見,理解這個概念會讓你在閱讀他人程式碼時更順暢。雖然本章只是「初探」,但完整的裝飾器教學會在後續進階章節展開。
小結:函式進階的學習路徑
本章介紹了不少工具,乍看有些凌亂,但其實它們圍繞著同一個主題——「讓函式更靈活」。*args 與 **kwargs 解決的是「輸入的彈性」;LEGB 規則解決的是「變數的可見性」;lambda 與遞迴解決的是「函式設計的多種風格」;把函式當作參數、裝飾器初探則是「函式組合」的可能性。
建議讀者寫完本章後,可以用「學到一個新工具就重構一段舊程式碼」的方式練習。例如把 Day 6 的猜數字遊戲中的部分邏輯抽出成函式,用 *args 讓它能接受多個難度等級等。透過這樣的練習,你會更深刻體會到這些技巧的實用價值。
結語
今天我們認識了 Python 函式的進階主題:*args 與 **kwargs 提供了極高的參數彈性;LEGB 規則讓我們能清楚掌握變數的作用域;lambda 適合用在簡短邏輯;遞迴則提供了一種拆解問題的思維方式。掌握這些技巧後,你已經能寫出更具彈性、更模組化的程式碼。
明天,我們會把焦點轉到字串處理。字串是程式裡最常操作的資料之一,從切割、合併到正規表達式,這些工具會讓你在處理文字資料時更得心應手。
留言
張貼留言