Try   HackMD

Python 自訂函式

作者:王一哲
第1版:2021年10月21日
第2版:2023年4月9日
第3版:2025年3月28日,補充 C++ 的語法

基本觀念

我們可以利用自訂函式將某一段程式碼取個名字,並設定好它的回傳值,接下來就可以在後續的程式碼中直接呼叫自訂函式,這樣可以使程式碼較為簡潔。自訂函式的語法如下

def 函式名稱(輸入參數):
    函式內要執行的程式碼
    ...
    return 回傳值

自訂函式的關鍵字是 def,也就是 define 的前 3 個字母。函式名稱的部分不能使用系統保留字。輸入參數則是在呼叫函式時傳給函式的變數,如果不需要輸入參數時可以什麼都不加。return 之後是函式回傳值,可以沒有回傳值,也可以有多個回傳值,當程式執行完 return 之後就會離開自訂函式。


沒有輸入參數及回傳值的自訂函式

我們可以用以下的程式碼自訂名為 hello 的函式,這個函式不需要輸入參數,功能就是印出 Hello World!,也沒有回傳值。

def hello():
    print("Hello World!")

如果要在後續的程式中呼叫 hello,寫法為

hello()

請注意,hello 後面的 () 不能省略。執行時會輸出

Hello World!

如果用以下方式呼叫

for i in range(5):
    hello()

輸出為

Hello World!
Hello World!
Hello World!
Hello World!
Hello World!

補充 C++ 的寫法。

#include <iostream> using namespace std; void hello() { cout << "Hello World!\n"; return; // 可以不加 } int main() { for(int i=0; i<5; i++) hello(); return 0; }



有輸入參數及回傳值的自訂函式

我們可以用以下的程式碼自訂名為 sub 的函式,這個函式需要輸入參數a、b,功能是回傳 a-b 的計算結果。

def sub(a, b):
    return a-b

如果要在後續的程式中呼叫 sub,則輸入參數為 2、3,寫法為

sub(a=2, b=3)
sub(b=3, a=2)
sub(2, 3)

請注意,如果輸入參數不加上名稱時會按照輸入的順序分別傳給 a、b。執行時回傳值皆為

-1

在自訂函式時也可以設定輸入參數的預設值,例如

def sub(a=2, b=3):
    return a-b

呼叫 sub 時可以輸入參數,如果沒有輸入參數則會自動採用預設值,例如以下的寫法回傳值為 4。

sub(a=5, b=1)

若改為這個寫法則回傳值為 -1。

sub()

如果改成以下的寫法

def sub(a, b):
    return a-b, a, b

則回傳值會有 3 個。如果用以下方式呼叫 sub

c = sub(a=2, b=3)

則 c 的資料類別為元組 (tuple),變數值為

(-1, 2, 3)

如果想要將回傳值分別指定給變數 d、e、f,寫法為

d, e, f = sub(a=2, b=3)

d、e、f 的變數值分別是 -1、2、3。

補充 C++ 的寫法。

#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; }



函式的遞迴 (Recursion)

函式的遞迴是指於自訂函式中呼叫自己或是其它自訂函式,如果使用得當可以大幅減少程式碼,但是使用不當會讓使用者很難看出程式碼的運作流程。使用函式的遞迴,一定要留下遞迴出口,在符合條件時能夠離開自訂函式,才不會形成無窮迴圈。以下是3個於自訂函式中呼叫自己的例子。


加總 (Summation)

如果要計算從 1 加到 n 的正整數總合,可以定義以下的函式,將一個 for 迴圈包在裡面,回傳加總的結果。

def myfunc(n): total = 0 for i in range(1, n+1): total += i return total ans = myfunc(995) print(ans) # 印出 495510

使用函式遞迴的寫法如下:

def myfunc(n): if n == 1: return 1 return n + myfunc(n-1) ans = myfunc(995) print(ans) # 印出 495510

若用

F(n) 代表自訂函式,以
n=5
為例,以上的程式碼運作的流程為
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

但是遞迴的寫法有一個缺點,如果遞迴次數過多、深度太深,程式會停止運作。

補充 C++ 的寫法。

#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; }



階乘 (Factorial)

正整數的階乘是指小於、等於此正整數的所有正整數乘積,另外定義0的階乘為1,以下是幾個例子

0!=11!=12!=2×1=23!=3×2×1=64!=4×3×2×1=245!=5×4×3×2×1=120

由以上幾個例子可得

n!=n(n1)!

如果想要用 Python 寫出計算

n! 的函式,不使用函式遞迴的寫法如下:

def factorial(n): result = 1 for i in range(1, n+1): result *= i return result print(factorial(10)) # 印出 3628800

使用函式遞迴的寫法如下:

def factorial(n): if n == 0: return 1 return n*factorial(n-1) print(factorial(10)) # 印出 3628800

這個寫法不需要在自訂函式中包進一個 for 迴圈,若用

F(n) 代表自訂函式,以
5!
為例,運作流程如下:
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×F(0)=5×4×3×2×1×1=120


補充 C++ 的寫法。

#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; }

費波那契數列 (Fibonacci sequence)

若以

Fn 表式費波那契數列第
n
個值,由小到大依序為
F0=0F1=1F2=F1+F0=0+1=1F3=F2+F1=1+1=2F4=F3+F2=2+1=3F5=F4+F3=3+2=5F6=F5+F4=5+3=8

n2,則
Fn=Fn1+Fn2


如果想要用 Python 寫出計算

Fn 的函式,不使用函式遞迴的寫法如下:

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

使用函式遞迴的寫法如下:

def fibonacci(n): if n == 0: return 0 if n == 1: return 1 return fibonacci(n-1) + fibonacci(n-2) print(fibonacci(10)) # 印出 55

這個寫法不需要在自訂函式中包進一個 for 迴圈,而且幾乎就是按照數學定義寫自訂函式內容,反而比不使用函式遞迴的寫法簡單、易懂。

補充 C++ 的寫法,不使用遞迴。

#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; }

遞迴。

#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; }

於自訂函式中呼叫另一個自訂函式的遞迴

我通常不會這樣寫程式,這會使程式碼難以理解。以下的例子來源為 APCS 105年3月觀念題第14題,再改寫成 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)

因為代入的數值很大,要先找出運作規律,例如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++ 程式碼則可以得到運算結果。

#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); }



使用遞迴可能會遇到的問題

有些題目的測資數量很多,會使得遞迴的次數過多、深度太深,程式碼在測試時可能會超時。以 ZeroJudge 基礎題 a216. 數數愛明明 為例,如果按照題目的定義使用函式遞迴,

f(n)=n+f(n1)     g(n)=f(n)+g(n1)

會寫出以下的程式碼,測試時會超時。

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))

但如果改用 C++,變數格式要使用 long,否則會溢位,使用時間約為 0.5 s,記憶體約為 328 kB,通過測試。

#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; }



所以遇到遞迴的題目時,可以試著找出一般式。先求

f(n),列出幾行算式
f(1)=1f(2)=2+f(1)f(3)=3+f(2)f(n)=n+f(n1)

將以上式子相加可得

f(n)=1+2+3++n=12n(n+1)

接下來求

g(n),列出幾行算式
g(1)=1g(2)=f(2)+g(1)g(3)=f(3)+g(2)g(n)=f(n)+g(n1)

將以上式子相加可得

g(n)=f(1)+f(2)+f(3)++f(n)=12i=1n(i2+i)=12[16n(n+1)(2n+1)+12n(n+1)]=112n(n+1)[(2n+1)+3]=16n(n+1)(n+2)

直接將測資指定的

n 代入一般式,通常會立刻得到答案。以下的 Python 程式碼在 ZeroJudge 測試時,使用時間約為 18 ms,記憶體約為 3.3 MB,通過測試。

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))

以下的 C++ 程式碼在 ZeroJudge 測試時,使用時間約為 2 ms,記憶體約為 328 kB,通過測試。

#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; }


tags:PythonC++