先看一段很多人都寫過的程式。三個科目,各要算人數、平均、最高分:
math_scores = [88, 72, 95, 60, 78]
eng_scores = [70, 85, 91, 66, 80]
sci_scores = [90, 78, 84, 72, 95]
total = sum(math_scores)
print(f'數學 共 {len(math_scores)} 人,平均 {total / len(math_scores):.1f} 分,最高 {max(math_scores)} 分')
total = sum(eng_scores)
print(f'英文 共 {len(eng_scores)} 人,平均 {total / len(eng_scores):.1f} 分,最高 {max(eng_scores)} 分')
total = sum(sci_scores)
print(f'自然 共 {len(sci_scores)} 人,平均 {total / len(sci_scores):.1f} 分,最高 {max(sci_scores)} 分')數學 共 5 人,平均 78.6 分,最高 95 分
英文 共 5 人,平均 78.4 分,最高 91 分
自然 共 5 人,平均 83.8 分,最高 95 分程式能跑,結果也對。問題在於同一件事寫了三次——如果老師說平均要改成取到小數第二位,你得改三個地方;漏改一個,畫面上就會有一行格式不一樣,而且不會有任何錯誤訊息提醒你。再加兩科就是五份,改一次要動五個地方。
函式(function)就是為了這件事存在的:把一段會重複用到的程式碼取個名字,之後用名字呼叫它。
第一個函式:def 把程式碼包起來
語法只有兩個要素——def 加上名字,然後縮排寫進要執行的內容:
def say_hello():
print('哈囉,歡迎使用成績系統')
say_hello()
say_hello()哈囉,歡迎使用成績系統
哈囉,歡迎使用成績系統有三件事要留意。
def 那一行只是「定義」,不會執行。Python 讀到 def say_hello(): 時只是把這段程式碼記起來、掛上 say_hello 這個名字,裡面的 print 一次都不會跑。真正執行是在你寫 say_hello() 的時候——名字後面那對小括號才是「呼叫」。少了括號變成 say_hello,Python 會以為你只是在提到這個函式本身,什麼都不會發生。
縮排決定範圍。def 下面縮排的每一行都屬於這個函式,回到不縮排就是離開函式了。這跟 if、for 的規則一樣。
函式要先定義才能呼叫。Python 由上往下執行,把呼叫寫在 def 前面會得到 NameError。
參數與回傳值:函式的輸入與輸出
上面那個函式每次都印一樣的字,用處不大。要讓它有變化,就得能從外面把資料傳進去——這就是參數:
def greet(name):
print(f'哈囉,{name}!')
greet('小明')
greet('雅婷')哈囉,小明!
哈囉,雅婷!name 是參數,寫在定義的括號裡;呼叫時放進去的 '小明' 就會變成函式裡的 name。
但真正讓函式好用的是另一半:回傳值。函式不只能印東西,還能把「算完的結果交回來」給呼叫的人:
def average(scores):
return sum(scores) / len(scores)
math_scores = [88, 72, 95, 60, 78]
eng_scores = [70, 85, 91, 66, 80]
a = average(math_scores)
print(a)
print(round(average(eng_scores), 1))
print(f'數學平均 {average(math_scores):.1f},英文平均 {average(eng_scores):.1f}')78.6
78.4
數學平均 78.6,英文平均 78.4return 的意思是「把這個值送回呼叫的地方,然後結束這個函式」。因為 average(...) 會變成一個實際的數字,你可以拿它去做任何事——存進變數、再丟給 round()、直接塞進 f-string。
print 和 return 是兩件事
這是新手最常混淆的地方。把上面的函式改成用 print 而不是 return:
def average_print(scores):
print(sum(scores) / len(scores))
result = average_print([88, 72, 95, 60, 78])
print(result)78.6
None數字有印出來,但 result 是 None。因為 print 只是把東西顯示在畫面上,函式本身沒有交回任何值,Python 就給你 None(代表「什麼都沒有」)。
判準很單純:畫面要看到的用 print,程式要拿去繼續算的用 return。實務上絕大多數函式應該 return,因為一旦寫死成 print,你就沒辦法拿這個結果去做別的事了。
預設參數:讓某些輸入變成選填
參數可以先給一個預設值,呼叫時不傳就用預設的:
def report(scores, subject='科目'):
print(f'{subject} 平均 {average(scores):.1f} 分')
report(math_scores)
report(math_scores, '數學')科目 平均 78.6 分
數學 平均 78.6 分有預設值的參數必須放在沒有預設值的後面,否則 Python 會直接報語法錯誤——因為它無法判斷你少傳的那個到底是誰。
作用範圍:函式裡的變數,外面看不到
這一段是函式最容易踩雷的地方,值得單獨看清楚。
count = 0
def add_one():
count = 100
print('函式內的 count =', count)
add_one()
print('函式外的 count =', count)函式內的 count = 100
函式外的 count = 0函式裡明明把 count 設成 100,外面卻還是 0。原因是在函式裡賦值,建立的是一個「只屬於這個函式」的新變數,跟外面那個同名的 count 是兩個不同的東西。函式執行完,裡面那個就消失了。
這種只在函式內部存在的變數叫區域變數(local variable),外面完全存取不到:
def calc():
temp = 999
calc()
print(temp)NameError: name 'temp' is not defined反過來,函式讀得到外面的變數:
school = '中山國中'
def show_school():
print('學校:', school)
show_school()學校: 中山國中所以規則是「讀得到,但改不到」——函式可以讀取外面的變數,但一旦在函式裡對它賦值,就會變成新建一個區域變數,而不是修改外面那個。
這聽起來很囉唆,但它其實是在保護你:因為函式裡的變數不會外洩,你在寫函式時可以放心用 i、temp、total 這些菜市場名,不必擔心撞到別的地方正在用的變數。這正是函式能被安心重複使用的原因。
(真的需要在函式裡修改外部變數時有 global 這個關鍵字,但初學階段請盡量不要用——那會讓程式變得難以追蹤是誰改了什麼。需要把結果帶出來,用 return 才是正解。)
實戰:把成績統計程式重構成函式
回到開頭那段重複三次的程式。現在把它拆成幾個各司其職的函式:
def average(scores):
return sum(scores) / len(scores)
def top_student(names, scores):
best = max(scores)
return names[scores.index(best)], best
def pass_list(names, scores, line=70):
passed = []
for i in range(len(scores)):
if scores[i] >= line:
passed.append(names[i])
return passed
def report_subject(subject, names, scores):
print(f'--- {subject} ---')
print(f'人數 {len(scores)} 人,平均 {average(scores):.1f} 分')
name, score = top_student(names, scores)
print(f'最高分:{name} {score} 分')
passed = pass_list(names, scores)
print(f'及格 {len(passed)} 人:{passed}')
names = ['小明', '小華', '阿哲', '雅婷', '子豪']
math_scores = [88, 72, 95, 60, 78]
eng_scores = [70, 85, 91, 66, 80]
sci_scores = [90, 78, 84, 72, 95]
report_subject('數學', names, math_scores)
report_subject('英文', names, eng_scores)
report_subject('自然', names, sci_scores)--- 數學 ---
人數 5 人,平均 78.6 分
最高分:阿哲 95 分
及格 4 人:['小明', '小華', '阿哲', '子豪']
--- 英文 ---
人數 5 人,平均 78.4 分
最高分:阿哲 91 分
及格 4 人:['小明', '小華', '阿哲', '子豪']
--- 自然 ---
人數 5 人,平均 83.8 分
最高分:子豪 95 分
及格 5 人:['小明', '小華', '阿哲', '雅婷', '子豪']三行 report_subject(...) 取代了原本三大段重複的程式碼。要加第四科,就再寫一行;要改平均的小數位數,只改 report_subject 裡的一個 .1f,三科同時生效。
有兩個地方值得多看一眼。
top_student 用 return names[...], best 一次回傳兩個值——Python 允許用逗號回傳多個值(實際上是包成一個 tuple),接的時候寫 name, score = top_student(...) 就會自動拆開。這在「一個函式要交回兩三個相關結果」時非常好用。
report_subject 裡面呼叫了 average、top_student、pass_list——函式可以呼叫別的函式。這是實務上組織程式的主要方式:小函式各做一件明確的事,大函式負責把它們串起來。你之後看到的任何一個像樣的專案,骨架都是這個形狀。
什麼時候該抽成函式
不必每三行就包一個函式,但遇到下面兩種情況就該動手:
同一段邏輯出現第二次的時候。第一次寫可以直接寫,第二次要複製貼上時就停下來抽成函式——這是最實用的判準,比任何理論都好記。
一段程式碼需要一句註解才說得清楚在做什麼的時候。那句註解通常就是函式的好名字。與其寫 # 找出最高分的學生,不如寫 def top_student(...),名字本身就是註解,而且不會像註解那樣改了程式忘了改。
寫到這裡,你已經有能力把前面幾篇的程式碼整理乾淨了——串列統計那支可以拆成函式,商品管理程式的選單與 CRUD 也可以,防呆那篇的 ask_choice() 其實就是一個函式。回頭把它們重構一遍,是很好的練習。