# 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++`