函數 - 進階篇
函數能介紹的內容實在太多,有的主題我自己覺得很有趣但可能稍微有點複雜,而且平常工作可能不一定會用的上,所以另外再開一個章節來介紹,也就是說,這個章節的內容先跳過也沒關係,如果晚上睡不著再翻開來看就好。
在開始介紹函數相關的內容之前,我們先來認識兩個名詞,一個叫「Expression(表達式)」,另一個叫「Statement(陳述句)」。
表達式 vs 陳述句
我們一般人類用的語言,不管是英文、日文還是中文都差不多,都有很多的詞組或片語(Phrase),而一個完整的句子是由這些片語組合而成。例如「我喜歡吃火鍋」這句話,其中「喜歡」、「吃」跟「火鍋」雖然你知道這些代表什麼意思,但都不能算是一個完整的句子,只能算是單字片語(Phrase)。如果對比到電腦程式語言來說,這些單字片語就是「表達式(Expression)」。雖然有些單字片語本身就能夠表達意思,但通常要把整個句字從頭到尾講完,才算的上是個完整句子;以電腦程式語言來說,完整的一句話就是「陳述句(Statement)」。
如果這樣還是有些抽象,來些例子吧 :
18
"hello kitty"
1450 > 9527
從最簡單的數字、字串,到四則運算,或是變數本身或函數呼叫,這些都是表達式,例如上例中數字 18
、字串 "hello kitty"
,以及 1450 > 9527
的計算結果,都是一個表達式,執行表達式之後都會得到結果或一個值,我們直接進 Python 的 REPL 看看就知道:
>>> 18
18
>>> "hello kitty"
'hello kitty'
>>> 1450 > 9527
False
如果只是一般的值,它的結果就是它本身,但如果是一個運算式,則會得到運算之後的結果,例如 1450 > 9527
的結果是 False
。
我們再看看下面這個例子:
cats = 5
if cats > 0:
print("有好多貓 🐈")
第一行宣告了一個 cat
變數並且指定值等於數字 5
,這是一個陳述句;接下來的 if
判斷句,也是一個陳述句。
如果用人類的語言來比喻,「鮪魚壽司 🍣」就是一個表達式,它就是代表「鮪魚壽司 🍣」這個東西,它可能沒辦法完整呈現你想要表達的意思;相對的「我要吃鮪魚壽司 🍣」就是一個陳述句,它可以完整的表示你想說的話。
就像我們講一句話,通常一個句子裡是由許多單字組合而成一樣,例如「我想要吃鮪魚壽司 🍣」這句話是一句完整的陳述句,但裡面有「我」、「想要」、「吃」、「鮪魚壽司 🍣」這些表達式,也就是說,一個陳述句通常會包括一個或多個表達式。
因為陳述句可以只包含一個表達式,所以就算只有單個數字 1450
,像這樣:
>>> 1450
1450
這是一個表達式,也是一個有效的陳述句,但反過來就不能這樣講了,陳述句並不是一個表達式,而是包含一個或多個表達式。
在上面的例子中,cats = 5
這行,等號右手邊的數字 5
是一個表達式,這個表達式的結果就是數字 5
,而 cats = 5
是指把等號右手邊的結果,也就是 5
,指定給一個變數 cats
,這個指定的行為是一個陳述句。
同樣的,if cats > 0:
這行,cats > 0
是一個表達式,這會得到一個布林值的結果,而 if cats > 0:
這整行或這整段的 if
判斷句則是一個陳述句。
不知道到這裡會不會看的有點眼花了,對程式新手來說,表達式跟陳述句一開始可能不是那麼容易分辨,別擔心,這也不影響程式學習,暫時可以不用太揪結這兩者在定義上有什麼不同。如果要說這兩者比較明顯的差異,在於表達式會有「結果」,但陳述句不會。這裡我很想用「回傳值(Return Value)」來替代「結果」,但用回傳值來表示表達式的結果又不夠精準,所以我這裡就還是使用「結果」代替大家比較常聽到的回傳值。
我們再回到 REPL 看個例子:
>>> char_count = len("hello")
>>> char_count = char_count + 1
>>> char_count
6
在第一行等號右手邊的 len()
函數呼叫是一個表達式,它會計算 "hello"
字串的字數並且回傳 5
,所以這個表達式的結果就是 5,而整個句子 char_count = len("hello")
是一個陳述句,它不會有結果。同樣的,接下來的 char_count = char_count + 1
這行,等號右手邊的 char_count + 1
是一個表達式,這個表達式的結果是 6
,但整行的 char_count = char_count + 1
是個陳述句,也同樣沒有結果,就只是一個行為而已。所以在 REPL 裡會看到前兩行都沒有印出結果。
但第三行的 char_count
是個表達式,它的結果就是 6
,所以在 REPL 裡會印出 6
。
表達式跟陳述句這兩個名詞並不是 Python 發明的,在很多程式語言裡面都有這個概念,只是你可能不知道你寫的就是表達式或陳述句而已。在 Python 裡常寫到的 if
、for
、while
、def
、class
這些關鍵字都是陳述句 ,也就是說像是邏輯判斷、迴圈、定義函數、定義類別這些行為本身都沒有結果,就只是個行為而已。
Lambda 表達式
我們在 Python 裡定義函數是使用 def
關鍵字,像這樣:
def add(a, b):
return a + b
我們會說這是定義一個 add()
函數,因為在 Python 裡的函數也是物件,也是我們在前個章節介紹過的「一等公民」,所以你也能把上面這兩行解讀成「建立一個函數物件,並且把它指定給 add
變數」。
事實上,如果你用 Python 內建的 dis
模組來檢視編譯出來的 Bytecode 的話,你會看到這個:
$ python -m dis demo.py
0 0 RESUME 0
1 2 LOAD_CONST 0 (<code object add>)
4 MAKE_FUNCTION 0
6 STORE_NAME 0 (add)
8 RETURN_CONST 1 (None)
我們不用看懂每個指令,但大概從名字可以猜是怎麼回事。首先,Python 先使用 LOAD_CONST
指令把要執行的程式碼變成一顆程式碼物件(Code Object),接著使用 MAKE_FUNCTION
指令把這個程式碼物件建立成函數,然後再用 STORE_NAME
指令把它指定給 add
變數。附帶一提,Python 自帶的這個 dis
模組是個很酷的工具,對想要了解 Python 是怎麼運作的人來說是個很好的入門工具。
講到函數是一等公民這件事,在 JavaScript 的世界裡函數也是物件,所以常會看到這樣寫:
// 使用 function 定義函數
const add = function (a, b) {
return a + b
}
// 或是使用箭頭函數
const add = (a, b) => a + b
不管是用 function
關鍵字或是使用箭頭函數(Arrow Function)的寫法,都能做到把函數指定給某個變數或常數。在 Python 裡函數雖然也是物件,但卻沒辦法像 JavaScript 一樣直接用 def
關鍵字把函數指定給一個變數。Python 有「Lambda 表達式(Lambda Expression)」可以做到一樣的事,使用的是關鍵字 lambda
,語法寫起來滿簡單的:
lambda 參數列表: 表達式
lambda
關鍵字後面接的是參數,跟一般函數一樣想放幾個參數都可以,參數超過一個的話就用逗號分開即可,然後再接一個冒號 :
,冒號後面就是實際要執行的內容。你應該有發現使用 Lambda 表達式的時候不需要像 def
定義函數一樣先幫函數想個名字,所以我們也會稱這種函數為「匿名函數」(Anonymous Function)。因為它沒有名字,lambda
只是個表達式而已,它的結果就是這個函數本身,所以通常會把這個結果指定給某個變數,例如:
>>> add = lambda a, b: a + b
>>> add(1, 2)
3
使用的時候就像一般的函數一樣搭配小括號 ()
來呼叫就行了。如果試著印出它的型態,會發現 Lambda 其實就只是個函數物件:
>>> type(add)
<class 'function'>
甚至如果我們使用內建的 dis
模組來檢視編譯出來的 Bytecode 的話,像這樣:
from dis import dis
add1 = lambda a, b: a + b
def add2(a, b):
return a + b
dis(add1)
dis(add2)
會發現 Lambda 表達式寫出來的函數跟一般 def
寫出來的行為是一樣的:
3 0 RESUME 0
2 LOAD_FAST 0 (a)
4 LOAD_FAST 1 (b)
6 BINARY_OP 0 (+)
10 RETURN_VALUE
6 0 RESUME 0
7 2 LOAD_FAST 0 (a)
4 LOAD_FAST 1 (b)
6 BINARY_OP 0 (+)
10 RETURN_VALUE
lambda
是另一種在 Python 定義函數的方式,只是它沒有名字而已:
add1 = lambda a, b: a + b
def add2(a, b):
return a + b
把函數的名字印出來看看:
>>> add1.__name__
'<lambda>'
>>> add2.__name__
'add2'
Lambda 表達式產生的函數因為沒有自己的名字所以只會印出 <lambda>
,而一般函數則是看的出來名字的 add2
。
另外,在 Lambda 表達式的冒號 :
右手邊也是一個表達式,不用寫 return
關鍵字就會自動會回傳這個表達式的結果,如果硬是加上 return
反而會造成語法錯誤。
如果不想要帶參數的話,可以這樣寫:
>>> say_hello_to_kitty = lambda: 'Hello, Kitty!'
>>> say_hello_to_kitty()
'Hello, Kitty!'
使用限制
Lambda 表達式雖然跟一般函數差不多,但它有一些限制,例如它只能有一個表達式,講的更白話一點就是它只能做比較簡單的運算,如果要做一些較複雜的事情,就不太適合用 Lambda 表達式了。別誤會,Lambda 表達式不是不能寫複雜的東西,但要把比較複雜的流程判斷硬是寫成一個表達式會讓程式碼變得不好閱讀,這也不是 Lambda 表達式的設計初衷。
其次,在 Lambda 表達式裡不能做賦值或宣告變數,像這樣寫:
lambda: n = 1
這會造成語法錯誤 SyntaxError: cannot assign to lambda
,也就是說,像底下這樣看起來似乎沒什麼問題的 Lambda 表達式:
add_one = lambda n: n += 1
這同樣也會造成語法錯誤,正確的寫法應該是:
add_one = lambda n: n + 1
另外,Lambda 表達式也不能使用型別註記,像這樣:
add_one = lambda n:int: n + 1
這一堆冒號看不懂啦!Python 直接就賞了我們一個語法錯誤。