函式(function)


前言

在前面的課堂中我們練習了許多的程式實作練習,然而到後來我們會發現有許多的程式碼事實上是類似甚至可以重複使用的。若是可以將這些程式都組織起來,重複利用,將可以讓程式更為簡潔易懂且更容易維護,這時候就是函式(function)上場的時候了!

函式(function)是不管任何程式語言中都非常重要的觀念,它的一些特性其實和數學上的函數其實有點像:即便你不知道函數裡面內容的實作細節(黑盒子),你仍可以操作使用它:傳入參數、取得對應回傳值(在 Python 程式語言中函式不一定要有參數傳入,回傳值可能有多個,也有可能為 None)。

圖片來源

認識函式

在建立複雜的程式時,通常我們會把功能拆解成一段段小程式,以利於抽象化、重複使用和方便測試程式,而將程式依據功用拆解成不同函式則是讓你的程式邁向模組化(modularized)的第一步(關於模組和套件我們會在後面的課程進一步說明)。而函式(function)則是模組化的非常重要的基礎。

一般來說常見的函式可以分為三種:

  1. 內建函式:在 Python 程式語言中內建的函式,不用安裝可以直接使用,有些需要引入標準函式庫才能使用。例如:len() 可以回傳物件內容長度。int() 可以轉換成整數型別、print() 可以列印出訊息、abs() 可以取得絕對值等。若是想要產生隨機值,可以引入 import random,若有想要了解的內建函式使用方式,可以透過網路查詢 Python 官方網站的文件就可以了解
  2. 第三方模組函式:需要透過套件管理工具(例如:pip)安裝後,才能引入使用。例如在終端機(terminal)輸入 pip install request 可以安裝 requests 這個 Python 常用來發出 http request 可以簡化想成是讓你用程式可以發出網頁資源的請求的套件(我們未來在討論模組設計時會進一步討論)。
  3. 使用者自定義函式:除了使用別人定義或是系統內建的函式外,還可以使用使用者自定義的函式(未來也可以提供給別的開發者/工程師使用),這也是我們接著會主要介紹的部分

一般來說我們在使用函式有兩個步驟:

  1. 定義函式(包含函式傳入參數、回傳值、函式執行內容、docstring 文字字串說明)
  2. 使用函式(呼叫函式)

接下來我們將來建立你的第一個函式!

隨堂練習:使用內建函式

我們在上面說明了函式主要有三種使用方式,請試著執行看看以下程式,感受一下引入內建函式的感覺。

建立你的第一個函式

接著我們來建立我們的第一個函式!在思考如何撰寫自定義的函式時需要思考幾個問題:

  1. 這個函式的功用是什麼?(這也攸關你的參數和命名設計)
  2. 函式需要哪些輸入哪些參數?
  3. 函式需要回傳哪些資料?

在 Python 程式語言中,我們可以使用 def 這個關鍵字來定義函式:

def 函式名稱(參數1, 參數2, 參數3, ...):
    """
    docstring 函式使用方式介紹
    """
    程式執行區塊
    return 回傳值

舉例來說,我們今天定義一個可以傳入兩個參數,回傳兩者相乘的函式。在開始定義函式之前先來看看若是不使用函式的寫法:

result_1 = 1 * 3
result_2 = 100 * 23

print(result_1)
print(result_2)

使用函式的寫法,是不是更加清晰而且可以重複使用程式碼:

# 定義函式,裡面使用 """ 撰寫的註解內容為 docstring 就是介紹函式如何使用的文件
def multiply(number1, number2):
    """
    :param number1: this is a first param
    :param number2: this is a second param
    :returns: this is a description of what is returned
    """
    return number1 * number2

# 呼叫使用函式
result_1 = multiply(1, 3)
result_2 = multiply(100, 23)

result_1 = multiply(1, 3)
result_2 = multiply(100, 23)
print(result_1)
print(result_2)

隨堂練習:建立你的第一個函式

請撰寫一個攝氏轉華氏的函式(運算公式為:F = 9/5 * C + 32),讓使用者可以傳入攝氏參數並回傳轉換華氏的結果。

文字字串說明(docstring)

通常我們在寫程式時,不會只有我們一個程式設計師/工程師,往往會需要和其他的工程師合作。此時,你撰寫的程式就需要考慮到如何和其他工程師合作,撰寫文件就是一件很重要的事情。所以我們除了要把我們程式撰寫的好閱讀、好維護外,我們也應該提供對應的文件讓其他開發者、工程師可以使用我們撰寫的程式。以 function 為例,最重要的就是函式命名和文字字串說明(docstring)。在 Python 函式中文字字串說明(docstring)是用來說明 function 如何使用的文件,通常使用 """ 將內容包含在裡面。

文字字串說明(docstring)通常會包含使用方式和功能、參數名稱和說明、參數資料型別、回傳值和說明及其資料型別等。進一步完整內容可以參考官方文件

def multiply(number1, number2):
    """This function will return the two numbers multiply value
    :param number1 (int): this is the first param
    :param number2 (int): this is the second param
    :returns (int): this is the multiply return value
    """
    return number1 * number2

我們也可以使用 help() 來查詢內建函式的文字說明,使用方式如下:

# help(查詢的內建函式),這邊查詢 len
help(len)

顯示查詢函式的文字說明內容:

Help on built-in function len in module builtins:

len(obj, /)
    Return the number of items in a container.

隨堂練習:文字字串說明(docstring)

請撰寫一個攝氏轉華氏的函式(公式為:F = 9/5 * C + 32),讓使用者可以傳入攝氏參數並回傳轉換華氏的結果,包含有 docstring。

傳入參數

函式在定義時會定義好要傳入哪些形式參數(paramter),而實際傳入的值則稱為實際參數(argument)值。

# number1, number2 為形式參數(paramter)
def multiply(number1, number2):
    return number1 * number2

# 1, 3 為實質參數(argument)
result_1 = multiply(1, 3)

值得注意的是若是你參數傳的是可變物件(例如:list、dict 等),那會因為修改到原來物件內容而影響到原本傳入的參數物件。

可變物件當作參數傳入會改變原有物件內容,所以可以使用複製值方式傳入

def get_the_first_element(user_list):
    user_list[0] = 'steven'
    return user_list[0]

# user_list 為實質參數(argument)
user_list = ['jack', 'tom', 'toby', 'marry', 'amy']
result_1 = get_the_first_element(user_list)

# steven
# ['steven', 'tom', 'toby', 'marry', 'amy'] 可以看到 user_list 也被更改影響到
print(result_1)
print(user_list)

若是我們在函式內先行複製一份參數(可變物件),可以避免更改到原有的物件內容:

def get_the_first_element(user_list):
    # 亦可使用 user_list = user_list[:] 將值進行複製
    user_list = user_list.copy()
    user_list[0] = 'steven'
    return user_list[0]

# user_list 為實質參數(argument)
user_list = ['jack', 'tom', 'toby', 'marry', 'amy']
result_1 = get_the_first_element(user_list)

# steven
# ['jack', 'tom', 'toby', 'marry', 'amy']
print(result_1)
print(user_list)

若是需要有預設值可以在定義參數時使用 = 填寫預設值,讓我們可以在參數沒有給定值時使用預設值:

def multiply(number1, number2=7):
    return number1 * number2

# 12 * 3 = 36
multiply(12, 3)

# number2 使用預設值:12 * 7 = 84
multiply(12)

隨堂練習:參數傳入

請執行以下程式感受一下可變物件和不可變物件傳入參數的感覺。

函式回傳值

Python 函式回傳值上可以使用 tuple 容器物件讓函式不只回傳一個值。

def get_the_first_and_last_element(user_list):
    user_list = user_list.copy()
    user_list[0] = 'steven'
    return (user_list[0], user_list[len(user_list) - 1])

user_list = ['jack', 'tom', 'toby', 'marry', 'amy']
(first_element, last_element) = get_the_first_and_last_element(user_list)

# steven, amy
# ['jack', 'tom', 'toby', 'marry', 'amy']
print(first_element, last_element)
print(user_list)

隨堂練習:函式回傳值

請執行以下程式感受一下多個函式回傳值的感覺。

使用函式

當定義好函式時,就可以呼叫函式來使用:一般是按照順序傳入的位置參數法(positional argument)或是可以使用指定關鍵字/指名參數法(keyword argument),但需要注意的是若是按照順序傳入參數或是指定關鍵字參數法混用的話,使用按照順序傳入的位置參數法的參數需要在前面否則會發生錯誤。

以下是使用函式範例:

def multiply(number1, number2):
    return number1 * number2

# 指定參數關鍵字:12 * 3 = 36
multiply(number1=12, number2=3)
# 指定參數關鍵字:3 * 12 = 36
multiply(number2=12, number1=3)

# 按照順序法:12 * 3 = 36
multiply(12, 3)
# 按照順序法:3 * 12 = 36
multiply(3, 12)

錯誤使用方式:

multiply(number2=3, 12)

  File "<stdin>", line 1
SyntaxError: positional argument follows keyword argument

使用按照順序傳入的位置參數法的參數需要在前面否則會發生錯誤

隨堂練習:設計傳入字串可以回傳字串長度的函式

請設計一個函式,讓使用的人可以傳入字串後可以回傳字串長度(不含 ,.)。

全域變數與區域變數

一般在函式內部使用的變數我們稱之為區域變數,其生命週期僅限於函式內。若是在函式外部定義的變數,整個程式都可以使用的則稱為全域變數 global。透過範例我們可以更清楚了解差異:

number_var = 10

def my_local_var_function():
    number_var = 23
    print(number_var)

# 印出區域變數 23
my_local_var_function()
# 印出全域變數 10,不受影響
print(number_var)

def my_global_var_function():
    global number_var
    # 印出全域變數 10
    print(number_var)

my_global_var_function()

隨堂練習:全域變數與區域變數

請執行以下程式感受一下全域變數與區域變數的感覺。

Decorator 裝飾器 / @ 語法糖(進階內容)

在 Python 程式中你可能會很常看到有程式使用 @ 在一個 function 函式上方。事實上,這是 Python 的語法糖(語法糖 Syntactic sugar 意思就是簡化寫法),也就是說簡化方便的寫法。意思就是可以把函式當作變數傳遞到另外一個函式中進行加工,把裝飾過的函式再當作回傳值回傳回來。

主要的概念就是把被裝飾函式傳入另外一個裝飾函式然後先做一些事情,再把裝飾後的函式回傳回來。

所以若執行該有加裝裝飾器的函式就會依序執行:裝飾函式 -> 傳入的函式(被裝飾函式)

接著透過一個簡單抽象化例子,我們可以一窺 Python Decorator 的樣貌:

def my_decorator(func):
    def wrap():
        print('裝飾器加料')
        # 執行傳入的函式
        func()
    return wrap

@my_decorator
def my_func():
    print('被裝飾函式')

# 執行 my_func 程式:先呼叫 wrap 後執行 my_func
my_func()

執行結果:

print('裝飾器加料')
print('被裝飾函式')

我們可以看到 @my_decorator 這個 Decorator 語法糖被加在 my_func 之上。而上面的程式碼其實等於,將 my_func 當做參數傳入 my_decorator 中,再被回傳回來:

def my_decorator(func):
    def wrap():
        print('裝飾器加料')
        # 執行傳入的函式
        func()
    return wrap

def my_func():
    print('被裝飾函式')

echo_my_func = my_decorator(my_func)

# 執行 echo_my_func 程式:先呼叫 wrap 後執行 func
echo_my_func()

執行結果:

print('裝飾器加料')
print('被裝飾函式')

看起來 Decorator 好像蠻方便的。但同學內心一定會開始思考究竟 Decorator 會很常使用嗎?或是會有了使用在哪些地方?等問題。事實上在實務上,Python 應用程式有許多地方都可以看到 Decorator 使用的蹤影,舉凡登入驗證(檢查哪些使用者可以看到某些內容)、日誌 logging 等地方(我們簡單使用 print 代表列印出訊息,一般實務上會引入 logging 這個套件使用)。

我們來看看以下的範例,這是在實務上蠻常見的範例,就是 log 紀錄。在這邊我們宣告了兩個函式。一個是 do_something,另一個是 use_logger

def use_logger(func):
    def warp():
        # 印出傳入參數 func 名稱
        print(f'Now use function {func.__name__}')
        func()
    return warp


@use_logger
def do_something():
    print('i am doing something')

do_something()

所以執行以上程式結果應該是(把 do_something 當作參數傳入 use_logger:當執行 do_something 時,會執行回傳的 warp 函式。所以就依序印出 Now use function do_something 和 do_something):

Now use function do_something
i am doing something

use_logger 這個很常見於實務上網站開發的例子,就是當使用者到達某個網頁時或執行某個 function 時能作個 log 統計紀錄。當我們有很多 function 或網頁想加入時就會重複寫很多次,若能透過 @ 裝飾器語法糖就能簡化寫法,隱藏裡面做的事情。

裝飾器對於初學者來講相對比較複雜抽象,需要親自練習才會比較有感覺。接下來我們在討論物件導向程式設計時,我們會再看到 @ 語法糖 的身影。若是第一次學習不熟悉也沒關係,不影響我們之後的專案實作或是日常的小程式開發,先記得有這個語法可以使用就好,未來有遇到再回來複習即可。

隨堂練習:Decorator 裝飾器 / @ 語法糖

請執行以下程式,感受一下 Decorator 裝飾器 / @ 語法糖 的使用方式。

總結

  1. 認識函式及其功能和不同種類的函式
  2. 自定義自己的函式參數和使用功能及回傳值
  3. 撰寫 doctstring 文件說明函式功用和使用方式
  4. 呼叫函式
  5. 全域變數與區域變數
  6. @ 語法糖

隨著我們的應用程式越長越大,我們需要將程式分解成功能不同的部分(才不會形成一大包程式很難閱讀和維護),讓我們可以重複使用我們的程式、便於維護和方便作測試。而接下來的類別和物件導向程式設計、模組套件內容,會更進一步討論如何讓我們的程式模組化。

參考文件

  1. Python 官方文件
  2. Python 标准库
  3. 理解 Python 装饰器看这一篇就够了


問題討論區
加入問題討論
作業任務區
提交作業任務