# Python 自訂函式 > 作者:王一哲 > 第1版:2021年10月21日 > 第2版:2023年4月9日 > 第3版:2025年3月28日,補充 C++ 的語法 ## 基本觀念 我們可以利用自訂函式將某一段程式碼取個名字,並設定好它的回傳值,接下來就可以在後續的程式碼中直接呼叫自訂函式,這樣可以使程式碼較為簡潔。自訂函式的語法如下 ```python def 函式名稱(輸入參數): 函式內要執行的程式碼 ... return 回傳值 ``` 自訂函式的關鍵字是 **def**,也就是 define 的前 3 個字母。函式名稱的部分不能使用系統保留字。輸入參數則是在呼叫函式時傳給函式的變數,如果不需要輸入參數時可以什麼都不加。**return** 之後是函式回傳值,可以沒有回傳值,也可以有多個回傳值,當程式執行完 return 之後就會離開自訂函式。 <br /><br /> ## 沒有輸入參數及回傳值的自訂函式 我們可以用以下的程式碼自訂名為 **hello** 的函式,這個函式不需要輸入參數,功能就是印出 Hello World!,也沒有回傳值。 ```python def hello(): print("Hello World!") ``` 如果要在後續的程式中呼叫 hello,寫法為 ```python hello() ``` 請注意,**hello 後面的 \(\) 不能省略**。執行時會輸出 ```python Hello World! ``` 如果用以下方式呼叫 ```python for i in range(5): hello() ``` 輸出為 ```python Hello World! Hello World! Hello World! Hello World! Hello World! ``` <br /> 補充 C++ 的寫法。 ```cpp= #include <iostream> using namespace std; void hello() { cout << "Hello World!\n"; return; // 可以不加 } int main() { for(int i=0; i<5; i++) hello(); return 0; } ``` <br /><br /> ## 有輸入參數及回傳值的自訂函式 我們可以用以下的程式碼自訂名為 **sub** 的函式,這個函式需要輸入參數a、b,功能是回傳 a-b 的計算結果。 ```python def sub(a, b): return a-b ``` 如果要在後續的程式中呼叫 sub,則輸入參數為 2、3,寫法為 ```python sub(a=2, b=3) sub(b=3, a=2) sub(2, 3) ``` 請注意,**如果輸入參數不加上名稱時會按照輸入的順序分別傳給 a、b**。執行時回傳值皆為 ```python -1 ``` <br /> 在自訂函式時也可以設定輸入參數的預設值,例如 ```python def sub(a=2, b=3): return a-b ``` 呼叫 sub 時可以輸入參數,如果沒有輸入參數則會自動採用預設值,例如以下的寫法回傳值為 4。 ```python sub(a=5, b=1) ``` 若改為這個寫法則回傳值為 -1。 ```python sub() ``` <br /> 如果改成以下的寫法 ```python def sub(a, b): return a-b, a, b ``` 則回傳值會有 3 個。如果用以下方式呼叫 sub ```python c = sub(a=2, b=3) ``` 則 c 的資料類別為**元組 (tuple)**,變數值為 ```python (-1, 2, 3) ``` 如果想要將回傳值分別指定給變數 d、e、f,寫法為 ```python d, e, f = sub(a=2, b=3) ``` d、e、f 的變數值分別是 -1、2、3。 <br /> 補充 C++ 的寫法。 ```cpp= #include <iostream> using namespace std; int sub(int a, int b) { // 參數沒有預設值 return a-b; } int sub2(int a=2, int b=3) { // 參數有預設值 return a-b; } int main() { cout << sub(2, 3) << "\n"; // 印出 -1 cout << sub2(5, 3) << "\n"; // 印出 2 cout << sub2() << "\n"; // 印出 -1 return 0; } ``` <br /><br /> ## 函式的遞迴 (Recursion) 函式的遞迴是指於自訂函式中呼叫自己或是其它自訂函式,如果使用得當可以大幅減少程式碼,但是使用不當會讓使用者很難看出程式碼的運作流程。使用函式的遞迴,一定要留下遞迴出口,在符合條件時能夠離開自訂函式,才不會形成無窮迴圈。以下是3個於自訂函式中呼叫自己的例子。 <br /><br /> ### 加總 (Summation) 如果要計算從 1 加到 n 的正整數總合,可以定義以下的函式,將一個 for 迴圈包在裡面,回傳加總的結果。 ```python= def myfunc(n): total = 0 for i in range(1, n+1): total += i return total ans = myfunc(995) print(ans) # 印出 495510 ``` <br /> 使用函式遞迴的寫法如下: ```python= def myfunc(n): if n == 1: return 1 return n + myfunc(n-1) ans = myfunc(995) print(ans) # 印出 495510 ``` <br /> 若用 $F(n)$ 代表自訂函式,以 $n = 5$ 為例,以上的程式碼運作的流程為 $$ \begin{align*} F(5) &= 5 + F(4)\\ &= 5 + 4 + F(3)\\ &= 5 + 4 + 3 + F(2)\\ &= 5 + 4 + 3 + 2 + F(1)\\ &= 5 + 4 + 3 + 2 + 1\\ &= 15 \end{align*} $$ 但是遞迴的寫法有一個缺點,如果遞迴次數過多、深度太深,程式會停止運作。 <br /> 補充 C++ 的寫法。 ```cpp= #include <iostream> using namespace std; int myfunc(int n) { int total = 0; for(int i=1; i<=n; i++) total += i; return total; } int myfunc2(int n) { if (n == 1) return 1; return n + myfunc2(n-1); } int main() { cout << myfunc(995) << "\n"; // 印出 495510 cout << myfunc2(995) << "\n"; // 印出 495510 return 0; } ``` <br /><br /> ### 階乘 (Factorial) 正整數的階乘是指小於、等於此正整數的所有正整數乘積,另外定義0的階乘為1,以下是幾個例子 $$ \begin{align*} 0! &= 1\\ 1! &= 1\\ 2! &= 2 \times 1 = 2\\ 3! &= 3 \times 2 \times 1 = 6\\ 4! &= 4 \times 3 \times 2 \times 1 = 24\\ 5! &= 5 \times 4 \times 3 \times 2 \times 1 = 120 \end{align*} $$ 由以上幾個例子可得 $$ n! = n \cdot (n-1)! $$ <br /> 如果想要用 Python 寫出計算 $n!$ 的函式,不使用函式遞迴的寫法如下: ```python= def factorial(n): result = 1 for i in range(1, n+1): result *= i return result print(factorial(10)) # 印出 3628800 ``` <br /> 使用函式遞迴的寫法如下: ```python= def factorial(n): if n == 0: return 1 return n*factorial(n-1) print(factorial(10)) # 印出 3628800 ``` <br /> 這個寫法不需要在自訂函式中包進一個 for 迴圈,若用 $F(n)$ 代表自訂函式,以 $5!$ 為例,運作流程如下: $$ \begin{align*} F(5) &= 5 \times F(4)\\ &= 5 \times 4 \times F(3)\\ &= 5 \times 4 \times 3 \times F(2)\\ &= 5 \times 4 \times 3 \times 2 \times F(1)\\ &= 5 \times 4 \times 3 \times 2 \times 1 \times F(0)\\ &= 5 \times 4 \times 3 \times 2 \times 1 \times 1\\ &= 120 \end{align*} $$ <br /> 補充 C++ 的寫法。 ```cpp= #include <iostream> using namespace std; int factorial(int n) { int result = 1; for(int i=1; i<=n; i++) result *= i; return result; } int factorial2(int n) { if (n == 0) return 1; return n * factorial2(n-1); } int main() { cout << factorial(10) << "\n"; // 印出 3628800 cout << factorial2(10) << "\n"; // 印出 3628800 return 0; } ``` <br /> ### 費波那契數列 (Fibonacci sequence) 若以 $F_n$ 表式費波那契數列第 $n$ 個值,由小到大依序為 $$ \begin{align*} F_0 &= 0\\ F_1 &= 1\\ F_2 &= F_1 + F_0 = 0 + 1 = 1\\ F_3 &= F_2 + F_1 = 1 + 1 = 2\\ F_4 &= F_3 + F_2 = 2 + 1 = 3\\ F_5 &= F_4 + F_3 = 3 + 2 = 5\\ F_6 &= F_5 + F_4 = 5 + 3 = 8 \end{align*} $$ 若 $n \geq 2$,則 $F_n = F_{n-1} + F_{n-2}$。 <br /> 如果想要用 Python 寫出計算 $F_n$ 的函式,不使用函式遞迴的寫法如下: ```python= def fibonacci(n): a, b = 0, 0 for i in range(1, n+1): if i == 1 or i == 2: a, b = 0, 1 else: a, b = b, a+b return a+b print(fibonacci(10)) # 印出 55 ``` <br /> 使用函式遞迴的寫法如下: ```python= def fibonacci(n): if n == 0: return 0 if n == 1: return 1 return fibonacci(n-1) + fibonacci(n-2) print(fibonacci(10)) # 印出 55 ``` <br /> 這個寫法不需要在自訂函式中包進一個 for 迴圈,而且幾乎就是按照數學定義寫自訂函式內容,反而比不使用函式遞迴的寫法簡單、易懂。 <br /> 補充 C++ 的寫法,不使用遞迴。 ```cpp= #include <iostream> using namespace std; int fibonacci(int n) { int a = 0, b = 0; for(int i=1; i<=n; i++) { if (i == 1 || i == 2) { a = 0; b = 1; } else { int c = a+b; a = b; b = c; } } return a+b; } int main() { cout << fibonacci(10) << "\n"; // 印出 55 return 0; } ``` 遞迴。 ```cpp= #include <iostream> using namespace std; int fibonacci(int n) { if (n == 0) return 0; if (n == 1) return 1; return fibonacci(n-1) + fibonacci(n-2); } int main() { cout << fibonacci(10) << "\n"; // 印出 55 return 0; } ``` <br /> ### 於自訂函式中呼叫另一個自訂函式的遞迴 我通常不會這樣寫程式,這會使程式碼難以理解。以下的例子來源為 [APCS 105年3月觀念題](https://apcs.csie.ntnu.edu.tw/wp-content/uploads/2018/12/1050305APCSconcept.pdf)第14題,再改寫成 Python 格式。 ```python= def foo(i): if i <= 5: print("foo: {:d}".format(i)) else: bar(i - 10) def bar(i): if i <= 10: print("bar: {:d}".format(i)) else: foo(i - 5) foo(6693) ``` <br /> 因為代入的數值很大,要先找出運作規律,例如foo(100) = bar(90) = foo(85) 或是 bar(100) = foo(95) = bar(85),如果經過 foo 及 bar 各1次,代入的數值減15,當代入的數值小於、等於20可能會結束遞迴。由於 6693 % 15 = 3,因此 foo(6693) = foo(18) = bar(8),8 <= 10,印出 bar: 8,結束遞迴。原版的題目中有一項是 foo(15106),如果用 Python 程式碼測試時,會遇到遞迴深度過深的問題,無法得到運算結果,但如果改寫成 C++ 程式碼則可以得到運算結果。 ```cpp= #include <iostream> using namespace std; void foo(int); void bar(int); int main() { foo(15106); bar(3091); foo(6693); return 0; } void foo(int i) { if (i <= 5) cout << "foo: " << i << endl; else bar(i - 10); } void bar(int i) { if (i <= 10) cout << "bar: " << i << endl; else foo(i - 5); } ``` <br /><br /> ### 使用遞迴可能會遇到的問題 有些題目的測資數量很多,會使得遞迴的次數過多、深度太深,程式碼在測試時可能會超時。以 ZeroJudge 基礎題 [a216. 數數愛明明](https://zerojudge.tw/ShowProblem?problemid=a216) 為例,如果按照題目的定義使用函式遞迴, $$ f(n) = n + f(n-1) ~~~~~ g(n) = f(n) + g(n-1) $$ 會寫出以下的程式碼,測試時會超時。 ```python= import sys def f(n): if n == 1: return 1 return n + f(n-1) def g(n): if n == 1: return 1 return f(n) + g(n-1) for line in sys.stdin: n = int(line) print(f(n), g(n)) ``` <br /> 但如果改用 C++,變數格式要使用 long,否則會溢位,使用時間約為 0.5 s,記憶體約為 328 kB,通過測試。 ```cpp= #include <iostream> using namespace std; long f(long n) { if (n == 1) return 1; return n + f(n-1); } long g(long n) { if (n == 1) return 1; return f(n) + g(n-1); } int main() { long n; while(cin >> n) cout << f(n) << " " << g(n) << endl; return 0; } ``` <br /><br /> 所以遇到遞迴的題目時,可以試著找出一般式。先求 $f(n)$,列出幾行算式 $$ \begin{align*} f(1) &= 1\\ f(2) &= 2 + f(1)\\ f(3) &= 3 + f(2)\\ & \vdots \\ f(n) &= n + f(n-1) \end{align*} $$ 將以上式子相加可得 $$ f(n) = 1 + 2 + 3 + \dots + n = \frac{1}{2} \cdot n(n+1) $$ 接下來求 $g(n)$,列出幾行算式 $$ \begin{align*} g(1) &= 1\\ g(2) &= f(2) + g(1)\\ g(3) &= f(3) + g(2)\\ & \vdots \\ g(n) &= f(n) + g(n-1) \end{align*} $$ 將以上式子相加可得 \begin{align*} g(n) &= f(1) + f(2) + f(3) + \dots + f(n)\\ &= \frac{1}{2} \sum_{i=1}^n (i^2 + i)\\ &= \frac{1}{2} \cdot \left [ \frac{1}{6} \cdot n(n+1)(2n+1) + \frac{1}{2} \cdot n(n+1) \right ]\\ &= \frac{1}{12} \cdot n(n+1) \left [ (2n+1) + 3 \right ] \\ &= \frac{1}{6} \cdot n(n+1)(n+2)\\ \end{align*} <br /> 直接將測資指定的 $n$ 代入一般式,通常會立刻得到答案。以下的 Python 程式碼在 ZeroJudge 測試時,使用時間約為 18 ms,記憶體約為 3.3 MB,通過測試。 ```python= import sys def f(n): return int(n*(n+1)/2) def g(n): return int(n*(n+1)*(n+2)/6) for line in sys.stdin: n = int(line) print(f(n), g(n)) ``` <br /> 以下的 C++ 程式碼在 ZeroJudge 測試時,使用時間約為 2 ms,記憶體約為 328 kB,通過測試。 ```cpp= #include <iostream> using namespace std; long f(long n) { return n*(n+1)/2; } long g(long n) { return n*(n+1)*(n+2)/6; } int main() { long n; while(cin >> n) cout << f(n) << " " << g(n) << endl; return 0; } ``` <br /> --- ###### tags:`Python`、`C++`