函數 - 基礎篇
撐了好久,終於來到這個章節了,這個章節應該算是本書我個人最喜歡的章節!幾乎每個程式語言都有函數,我們在前面的章節或多或少都有用過一些函數,用最多的大概是把東西印出來的 print()
函數了。但在開始介紹函數之前,我想先問大家一個很基本的問題:你認為「函數(Function)」是什麼?
函數是什麼?
如果你曾經寫過其他程式語言,不知道你是否曾想過這個最最最基本的問題?到底什麼是函數?因為學校老師或是書上說要寫函數就是用 function
或 def
給它寫下去,裡面可能還有什麼參數啦、回傳值什麼的...。
我上課的時候最喜歡問同學們這種「為什麼」的問題,例如「為什麼要寫函數?」,我通常會得到像是「因為函數可以重複使用」之類的答案。是沒錯啦,函數可以重複使用的確是使用函數的好處之一,但它並不是寫函數的目的。想想看,你是否有寫過一些函數,是根本沒有用第二次的?如果有,那你認為的「因為函數可以重複使用」這個說法就打自己臉了。所以,到底什麼是函數?
大家在國中或高中上數學課的時候有沒有看過這種東西:
f(x) = 3x + 2
當時數學老師會說這個叫做「一元一次方程式」,那個 x
叫做「代數」,如果 x
代 2 進去會得到 8,代 3 進去會得到 11,如果再代一次 2,還是會得到 8。這就是函數,前面那個 f 就是函數 function 的意思,只是大家可能沒意識到罷了(還是不願想起來?)
所以如果要我給「函數」一個定義,我會說:
函數是「輸入值」與「輸出值」之間的對應關係,而函數的名稱就是這個關係的名字。
把這些名詞換成程式語言的用語,輸入值就是「參數(parameter)」,而輸出值就是「回傳值(return value)」。一個好的函數,理想狀況是可以做到函數的輸出值只跟輸入值有關,只要是固定的輸入值,不管執行幾次,它的答案不會飄,都是固定的輸出值,不會因為時間、亂數或環境變數之類的「副作用(Side Effect)」而造成輸 出值不同。如果可以做到沒有副作用的函數,我們也會稱這樣的函數叫「純函數(Pure Function)」,不純免錢。
給函數一個好的名字很重要,最好做到一眼就看出來這個函數想做什麼事。之所以會撰寫函數,是因為我們可以透過函數把原本比較繁瑣的流程抽象出來,我們的腦細胞就可以把重點放在怎麼使用這個函數,而不需要關注函數本身實作的細節。命名是電腦科學界的兩大難題之一,有好的名字是很難的,光是命名就能寫一整本書了。
想要再深入了解這方面的主題,可用關鍵字「函數式程式設計(Functional Programming)」再找其他資料研究。
所以,為什麼要使用函數?因為函數可以把某部份的程式碼抽象化,讓程式碼更容易閱讀,說的更白話一點寫函數就是幫某一段程式碼的邏輯取個名字,讓我們不用閱讀函數的實作細節,只要看函數的名字就知道這個函數的用途。當然,函數可以重複使用也是一個好處,只是這就不太算寫函數的目的。
函數名稱的命名建議跟變數名稱一樣,規則也差不多,有些不該用或不能用的,例如 print
、input
這種原本就是 Python 的內建函數,用了之後原本的 print()
就不能用了;或是 True
、False
這些保留字,都不能用來當函數名稱。而且,畢竟函數的目的是幫某一段程式碼取名字,所以函數名稱要有意義,能一看就知道這個函數是做什麼的,如果用 a
、b
、c
這類看不太出來用途的名字,倒不如不要寫函數。
定義函數
在 Python 定義函數, 使用的是 def
關鍵字。在 def
後面接著函數的名稱以及一對小括號,最後別忘了加上冒號 :
,寫起來大概像這樣:
def say_hello():
print("Hello")
在 def
底下,就是函數的本體,也就是執行這個函數的時候實際上要做的事。有看到 在第二行 print()
前面的留白嗎?那是刻意的,這在程式語言裡叫做「縮排(Indentation)」?如果我故意把縮排拿掉,像這樣:
def say_hello():
print("Hello") # 這會出錯,沒辦法執行
執行之後會產生 IndentationError
的錯誤訊息告訴你縮排不正確,這算是 Python 的特色之一。因為 Python 不像其他程式語言使用大括號 { }
做為程式碼區塊(Block)的識別,僅能透過縮排的方式來做到這件事。縮排表示一個區塊或段落的開始,如果縮排裡面還有縮排就表示這個區塊還有子區塊,依此類推。適當的使用縮排可以讓程式碼看起來更有結構,也更容易閱讀。
如同在布林值與流程控制章節所提到的,對於那些在其他程式語言不太在意縮排習慣的人來說,跟你好好講說要縮排講不聽,現在就讓 Python 來教教你。
如果是比較簡單的函數,不想換行寫,想直接寫成一行也是可以:
def say_hello(): print("Hello")
只是不太推薦這樣寫,這樣會讓程式碼變得沒那麼好閱讀,不過畢竟這不是硬性規定,我也是有看過有些人就是喜歡這樣寫。然而,有時候暫時還沒想到要寫什麼,只是想先定義一個函數名稱,該怎麼做?
pass
Python 是強制規定要縮排的,也就是說,函數的本體不能是空的,甚至只放註解也不行:
def say_hello():
# 這裡要寫東西
# 但還沒想到要寫什麼
因為註解最後會被無視,所以這樣寫還是會產生 IndentationError
錯誤訊息。這時候可使用 pass
關鍵字先卡個位:
def say_hello():
pass
在前面介紹 if...else...
語法的時候也看過這個關鍵字,pass
就跟它字面上的意思一樣不做任何事,技術上來說甚至連編譯的時候它都會被忽略。pass
只是卡個位,在任何地方放上這個關鍵字都不會出問題,所以你想這樣寫也行:
def say_hello():
print("hi")
pass
print("hey")
在這種地方安插個 pass
沒什麼意義也不需要,不過在暫時還不知道要寫什麼內容的,像是空的 if...else...
、空的函數或是空的類別,都可以用先它卡個位。
參數與執行
再回來看一下數學的函數
f(x) = 3x + 2
這裡的 x
在數學我們會說它叫做代數,在程式設計領域我們會說它是個「參數(Parameter)」。函數不一定要有參數,像上面的 say_hello()
函數就是個例子。寫函數的目的是為了把一段邏輯或程式碼抽象化並且可以重複使用,但這抽象化也是有分等級的,如果能再把一些值帶進函數裡,讓這個函數變得更有彈性、更好用,抽象的程度會更高一點,也更容易重複被使用。
在 Python 中,函數的參數是放在小括號 ()
裡面,如果有好幾個參數,就用逗號 ,
隔開,例如我有一個可以把華氏溫度(Fahrenheit)轉換成攝氏溫度(Celsius)的計算函數:
# 華氏 -> 攝氏
def fahrenheit_to_celsius(temperature):
return (temperature - 32) * 5 / 9
return
的意思我們待會再看,先暫時把它當作是函數執行之後的輸出結果就好,這裡的 temperature
就是這個函數的參數。事實上,參數對這個函數本身來說 不只是參數,也同時是這個函數的區域變數,也就是說只有在這個函數裡面才能用,離開這函數之後就不存在了,就算有同名的變數也不是同一個變數,待會後面介紹到「範圍(Scope)」的時候會再詳述。參數的名字可以是任何合規定的變數名稱,只是為了可讀性,我們會盡量讓參數的名字看起來更容易被理解。
要執行函數的話,如果參數本身沒有額外定義參數,直接在函數名稱後面加上小括號 ()
就行了;如果函數有需要參數的時候,需要把值放在小括號裡一併帶進來給它:
# 沒有參數的
>>> say_hello()
# 有參數的
>>> fahrenheit_to_celsius(95)
35.0
>>> fahrenheit_to_celsius(27)
-2.7777777777777777
在上面看到的 95
跟 27
就是要帶給這個函數的參數,我們會把帶進去的值稱做「引數(Argument)」。參數跟引數很多時候會被混著講,但其實是有區別的,在定義函數的時候,放在小括號裡的是參數,在執行函數的時候,帶進小括號裡的是引數。
原本的函數如果設定了參數,不管定義幾個,在執行函數的時候就要給足夠數量的引數,不能多也不能少,否則會產生錯誤,例如:
# 不需要帶參數但硬給它一個
>>> say_hello("悟空")
TypeError: say_hello() takes 0 positional arguments but 1 was given
# 說好只要一個參數,但給了兩個
>>> fahrenheit_to_celsius(180, 200)
TypeError: fahrenheit_to_celsius() takes 1 positional argument but 2 were given
從錯誤訊息都可以看的出來,不管引數是多給還是少給,Python 會跟你抱怨引數數量不對,我覺得這很棒。另外,有注意到上面的錯誤訊息嗎?在錯誤訊息裡用的是 arguments
這個字,而不是 parameters
喔。
是執行還是呼叫?
我們現在知道怎麼定義函數了,如果想要讓這個函數「動起來」,有時候會聽到別人說「執行(Execute)」這個函數,有時候會說「呼叫(Call)」函數。這兩個動詞想要表達的意思其實差不多,只是立場有些不太一樣。
呼叫函數是指請求函數運行的動作或行為。例如,fahrenheit_to_celsius(95)
,就是「呼叫」函數 fahrenheit_to_celsius()
;而執行函數,是指函數實際進行運算的過程,也就是說,函數應該是被「呼叫」之後才會「執行」。
因此,這 兩個動詞分別表示函數使用過程的不同階段,也反映了看待函數的不同角度。「執行」強調函數的運行過程,「呼叫」強調從外部使用函數的行為,都是有效和常用的表述方式,我也常混著用。
關鍵字引數
另一個問題是,假設我寫了一個計算身體質量指數(Body Mass Index, BMI)的函數,並定義了 height
跟 weight
兩個參數,像這樣:
def calc_bmi(height, weight):
# 請想像在這裡做了很複雜的計算
print(height, weight)
我知道呼叫這個函數的時候要帶身高跟體重給它,但到底是哪個參數是身高哪個是體重?萬一不小心寫錯的話,算出來的答案可能是錯的。當然我也可以再查看原本的函數定義,確認參數的數量跟位置,但如果參數數量比較多的情況,用這種「一個蘿蔔一個坑」的方式來記憶參數的位置就有點麻煩。在 Python 中我們可以這樣寫:
>>> calc_bmi(170, 60)
170 60
>>> calc_bmi(weight=60, height=170)
170 60
>>> calc_bmi(height=170, weight=60)
170 60
執行函數的時候可以直接指定參數的名稱,甚至跟定義的參數位置不一樣也沒關係,執行的結果都是一樣的。這種引數的寫法叫做「關鍵字引數(Keyword Argument)」。相比之下原本需要按照順序、逐個傳入引數的方式又稱做「位置引數(Positional Argument)」。
因為我們是直接指定參數的名稱,這樣就不用煩惱要記得參數的位置,只要記得參數叫什麼名字就行了。關鍵字引數看起來好像比較清楚,這樣以後都應該使用關鍵字引數來傳參數嗎?也不是這樣,如果參數的個數只有一個,或是參數的位置容易記,用一般的位置引數反而比較簡單;相對的,但如果參數比較多,或者參數的位置不太容易記憶,那麼使用關鍵字引數會比較方便一點,不用特別去記參數的順序。
這兩種方式可以混著用,只是有一些遊戲規則要遵守。