Functions
Introduction
往下之前,請你回想 Repetition Structures - Nested Loop 中的例子:
輸入一個正整數 \(n\) ,輸出 \([1, n]\) 間的最大質數。
n = int ( input ())
for i in range ( n , 0 , - 1 ):
is_prime = True
for j in range ( 2 , i ):
if i % j == 0 :
is_prime = False
break
if is_prime :
print ( f " { i } is the largest prime in [1, { n } ]" )
break
這個程式碼的功能是正確的,但是程式碼的可讀性不高,因為程式碼的長度太長,並且程式碼的邏輯不夠清晰。
我們可以把判斷質數的部分抽出來,並且將他包裝成一個函式:
def is_prime ( x ):
for i in range ( 2 , x ):
if x % i == 0 :
return False
return True
n = int ( input ())
for i in range ( n , 0 , - 1 ):
if is_prime ( i ):
print ( f " { i } is the largest prime in [1, { n } ]" )
break
這樣的好處是,我們可以將相同的程式碼重複使用,並且可以讓程式碼更加簡潔。
那如果我想要進一步程式碼封裝成一個接受正整數 \(n\) 的函式,並且回傳 \([1, n]\) 間的最大質數呢?
def is_prime ( x ):
for i in range ( 2 , x ):
if x % i == 0 :
return False
return True
def largest_prime ( n ):
for i in range ( n , 0 , - 1 ):
if is_prime ( i ):
return i
n = int ( input ())
print ( f " { largest_prime ( n ) } is the largest prime in [1, { n } ]" )
不曉得你有沒有感受到,當程式碼越來越長,我們就越需要函式來幫助我們將程式碼分割成更小的部分,這樣我們就可以更容易的閱讀程式碼。
這個引子告訴我們,函式是一個可以將程式碼包裝成一個獨立的單位,並且可以重複使用的工具。
Define a Function
函式的定義是以 def
開頭,後面接著函式的名稱,以及括號內的參數(Parameter) ,縮排內的程式碼就是函式的內容。
你可以選擇是否要在函式中加上 return
來回傳值,如果沒有,函式則會回傳 None
。
舉個例子,定義一個函式 greet
,他接受一個參數 name
,並印出 Hello, {name}
:
def greet ( name ):
print ( f "Hello, { name } " )
print ( greet ( "World" ))
greet("World")
會先印出 Hello, World
,接著回傳 None
。
字串 "World"
被稱為引數(Argument) ,而 name
則被稱為參數(Parameter) 。但也不用太拘泥。
再舉個例子,定義一個函式 freq ( x )
接受一個數字字串 x
,回傳 0-9
的出現次數,以序對的方式回傳:
def freq ( x : str ) -> tuple :
table = [ 0 ] * 10
for digit in x :
table [ int ( digit )] += 1
return tuple ( table )
a , b = "114514" , "111445"
print ( freq ( a ) == freq ( b ))
我們可以在參數後面加上 :
來指定參數的型別,並且在函式後面加上 ->
來指定回傳值的型別。
Default Argument Values
有時候我們會希望函式的參數有預設值,這樣在呼叫函式時就不需要填入引數。
例如,函式 max_of(x, k=1)
接受兩個參數 x
串列與 k
數字,回傳 x
中最大的 k
個數字,以串列的方式回傳,而 k
的預設值為 1
:
def max_of ( x : list [ int ], k = 1 ):
return sorted ( x , reverse = True )[: k ]
y = [ 4 , 8 , 5 , 3 , 9 ]
print ( max_of ( y ))
print ( max_of ( y , 2 ))
print ( max_of ( y , k = 3 ))
再舉一個例子,函式 weight_score(math, english, programming, math_weight=0.25, english_weight=0.25, programming_weight=0.5)
接受三個參數 math
, english
, programming
以及三個預設值 math_weight=0.25
, english_weight=0.25
, programming_weight=0.5
,回傳加權分數:
def weight_score ( math , english , programming , math_weight = 0.25 , english_weight = 0.25 , programming_weight = 0.5 ):
return math * math_weight + english * english_weight + programming * programming_weight
sean_score = weight_score ( 80 , 90 , 100 )
yaris_score = weight_score ( math = 82 , math_weight = 0.3 , english = 90 , english_weight = 0.3 , programming = 100 ,
programming_weight = 0.4 )
print ( sean_score , yaris_score )
有預設值的參數必須放在沒有預設值的參數後面。但如果你在呼叫函式的時候,指定了參數的名稱,則可以不用遵守這個規則。
其實還有一些神奇的用法,但就不在這裡討論了。
Lambda Function
還記得 map
嗎? 他可以將一個函式應用到一個串列上。
先回憶一下,我們可以這樣使用 map
:
def square ( x ):
return x ** 2
lst = [ 1 , 2 , 3 , 4 , 5 ]
print ( list ( map ( square , lst )))
song_name = [ "I" , "Really" , "Want" , "to" , "Stay" , "At" , "Your" , "House" ]
print ( list ( map ( len , song_name )))
Output [1, 4, 9, 16, 25]
[1, 6, 4, 2, 4, 2, 4, 5]
I Really Want to Stay At Your House” by Rosa Walton
這種接受函式為參數的函式被稱為高階函式(Higher-Order Function) 。
但是,如果函式只會被使用一次,我們可以使用 lambda
來定義一個匿名函式:
lst = [ 1 , 2 , 3 , 4 , 5 ]
print ( list ( map ( lambda x : x ** 2 , lst )))
再舉個例子,用過 min
嗎? 他可以找出一個串列中的最小值,我們也可以自訂規則,例如有一個座標串列 points
,我們可以這樣找出最靠近原點的點:
points = [( 9 , 2 ), ( 3 , 4 ), ( 5 , 6 ), ( 7 , 8 ), ( 4 , 8 ), ( 1 , 3 )]
print ( min ( points , key = lambda x : x [ 0 ] ** 2 + x [ 1 ] ** 2 ))
再舉個例子,兩個串列 a
與 b
,我們可以這樣計算兩個串列的內積:
a , b = [ 1 , 2 , 3 ], [ 4 , 5 , 6 ]
print ( sum ( map ( lambda x , y : x * y , a , b )))
Factory Function
有時候我們會希望函式回傳另一個函式,這樣的函式被稱為工廠函式(Factory Function) 。
舉個例子,定義一個函式 make_power_fun ( n )
,他接受一個數字 n
,回傳一個函式,這個函式接受一個數字 x
,回傳 x
的 n
次方:
def make_power_fun ( n ):
return lambda x : x ** n
power_fun_2 = make_power_fun ( 2 )
power_fun_3 = make_power_fun ( 4 )
print ( power_fun_2 ( 3 ))
print ( power_fun_3 ( 3 ))
這樣的好處是,我們可以在不同的地方使用不同的 n
,而不需要重複定義函式。
Nested Function
函式可以被定義在另一個函式的內部,這樣的函式被稱為巢狀函式(Nested Function) 。
例如,函式 collect_anagrams(words, target)
接受一個字串串列 words
與一個字串 target
,回傳 words
中與互為 target
是相同字母異序詞 的串列:
於是我在函式的內部先定義了一個輔助函式(Helper Function) is_anagram ( x , y )
,他接受兩個字串 x
與 y
,回傳 x
與 y
是否為相同字母異序詞:
def collect_anagrams ( words , target ):
def is_anagram ( x , y ):
return sorted ( x ) == sorted ( y )
return [ word for word in words if is_anagram ( word , target )]
strs = [ "now" , "won" , "own" , "no" , "on" , "www" ]
print ( collect_anagrams ( strs , "onw" ))
這個在刷 LeeCode 時會常常用到。
Recursion
來到本章的重頭戲,遞迴(Recursion) 。
遞迴是一種函式呼叫自己的技巧,這樣的函式被稱為遞迴函式(Recursive Function) 。
先來個經典的例子,計算 \(n!\) ,迴圈的寫法是這樣的:
def factorial ( n ):
result = 1
for i in range ( 1 , n + 1 ):
result *= i
return result
print ( factorial ( 0 ))
print ( factorial ( 5 ))
記得高中數學課本上的定義嗎?
\[
\text{factorial}(n) = \begin{cases} 1 & \text{if } n = 0 \text{, base case}\\ n \times \text{factorial}(n-1) & \text{if } n > 0 \text{, recursive case}\end{cases}
\]
這樣的定義就可以直接翻譯成程式碼:
def factorial ( n ):
if n == 0 :
return 1
else :
return n * factorial ( n - 1 )
print ( factorial ( 0 ))
print ( factorial ( 5 ))
當 n == 0
時,我們稱為基本情況(Base Case) ,這是遞迴的終止條件,當 n > 0
時,我們稱為遞迴情況(Recursive Case) ,這是遞迴的執行條件。
每一個遞迴函式都應該有一個基本情況,這樣的遞迴才會終止,否則就會陷入無窮遞迴,咦?有沒有很熟悉?還記得無窮迴圈嗎?
再舉個例子,計算費氏數列的第 \(n\) 項,迴圈的寫法是這樣的:
def fibonacci ( n ):
a , b = 0 , 1
for _ in range ( n ):
a , b = b , a + b
return a
print ( fibonacci ( 0 ))
print ( fibonacci ( 10 ))
同樣的,還記得高中數學課本上的定義嗎?
\[
\text{fibonacci}(n) = \begin{cases} 0 & \text{if } n = 0 \text{, base case}\\ 1 & \text{if } n = 1 \text{, base case}\\ \text{fibonacci}(n-1) + \text{fibonacci}(n-2) & \text{if } n > 1 \text{, recursive case}\end{cases}
\]
這樣的定義就可以直接翻譯成程式碼:
def fibonacci ( n ):
if n == 0 :
return 0
elif n == 1 :
return 1
else :
return fibonacci ( n - 1 ) + fibonacci ( n - 2 )
print ( fibonacci ( 0 ))
print ( fibonacci ( 10 ))
但是,遞迴的效率通常比迴圈低,因為遞迴會造成大量的函式呼叫,而且容易造成大量的重複計算。
再來一個經典的例子,計算整數 a, b
的最大公因數,你可能有聽過輾轉相除法,又稱為歐幾里得演算法(Euclidean algorithm) ,他的定義是這樣的:
\[
\text{gcd}(a, b) = \begin{cases} a & \text{if } b = 0 \text{, base case}\\ \text{gcd}(b, a \mod b) & \text{if } b > 0 \text{, recursive case}\end{cases}
\]
直接寫成程式碼:
def gcd ( a , b ):
if b == 0 :
return a
else :
return gcd ( b , a % b )
print ( gcd ( 12 , 18 ))
print ( gcd ( 18 , 12 ))
print ( gcd ( 4843 , 1234 ))
這個要寫成迭代(Iterative) 版本還比較難。
關於遞迴,就先講到這裡,未來進入演算法的章節時,會再深入討論,而且會有更多的例子。
Practice
Reference code
def f91 ( n ):
if n <= 100 :
return f91 ( f91 ( n + 11 ))
elif n >= 101 :
return n - 10
k = int ( input ())
n = list ( map ( int , input () . split ()))
for x in n :
print ( f91 ( x ))
你仔細觀察一下,其實可以寫成這樣:
def f91 ( z ):
if z >= 101 :
return z - 10
else :
return 91
k = int ( input ())
n = list ( map ( int , input () . split ()))
for x in n :
print ( f91 ( x ))
Reference code
MAX = 47
fibonacci = [ 0 ] * MAX
fibonacci [ 1 ] = 1
for i in range ( 2 , MAX ):
fibonacci [ i ] = fibonacci [ i - 1 ] + fibonacci [ i - 2 ]
while True :
n = int ( input ())
if n == - 1 :
break
print ( fibonacci [ n + 1 ])
這種寫法叫做動態規劃(Dynamic Programming) ,未來會再討論,而且會很頭痛。
Assignment
February 7, 2024
February 7, 2024
GitHub