約維安計畫:Python 的函數

第十六週。

約維安計畫:Python 起步走我們透過 print("Hello, World!") 來確認 Python 的開發環境及執行環境是否已經安裝妥當,在這一段哈囉世界的程式碼中,其實就能體驗程式設計與資料分析的一個核心精神:對資料應用函數

print 是函數的名稱,print() 則表示要應用該函數的功能,而 “Hello, World!“ 則是一個被稱呼為字串(String)的資料型別;換言之,哈囉世界可以表達為:對 “Hello, World!“ 這個字串應用了 print 函數。

print("Hello, World!")

什麼是函數

函數是構成所有程式語言的基本要素,一個函數是一段「被命名」的程式碼,這段程式碼可以用於執行某個特定任務,可能是數值的運算或者文字的處理,當使用者想要應用(使用、呼叫)某一個函數之前,必須先確定在作用域中該函數已經被定義或者被載入。簡而言之,在哈囉世界的程式碼中,之所以能夠對字串 “Hello, World!“ 應用 print 函數,是因為 print 函數屬於 Python 的內建函數,在 Python 啟動的當下就會被載入供我們使用,內建函數多達 74 個,所有的內建函數與詳細介紹可以參考:https://docs.python.org/3/library/functions.html

函數的兩種使用方式

在 Python 中最常見的函數使用方式是在函數名稱後接上小括號,並在小括號中輸入物件名稱(該物件所儲存的可能是某個資料型別或者資料結構)以及引數(如果函數有任何參數設計)。

function_name(OBJECT, ARGUMENTS)

由於 Python 屬於物件導向(Object-oriented)程式語言,第二種函數定義的形式是綁定於物件之上,這時候使用的方式就變成了在物件名稱後接上附屬符號(.)再接上函數名稱、小括號與引數(如果函數有任何參數設計),在第二種形式中,不只在使用上的語法相異,在稱呼上我們也不再用「函數」,而是改稱物件的「方法」。

OBJECT.method_name(ARGUMENTS)

例如 Python 有一個內建函數 sorted(),串列有一個方法 sort(),兩者都能夠排序一個串列,但是在使用的語法與意義上不同,使用內建函數 sorted() 排序串列是我們習慣且直觀的「對資料結構應用函數」,語法是 sorted(a_list_to_be_sorted);使用串列方法 sort() 排序是目前較陌生的「使用綁定於資料結構的方法」,語法是 a_list_to_be_sorted.sort()。這篇文章中我們專注於自行定義的函數,自行定義物件的方法我們會在「類別」主題時談到。

primes_list = [11, 7, 5, 3, 2]
sorted(primes_list) # Apply sorted function to primes_list primes_list.sort()  # Call sort method of primes_list

參數與引數的差別

在排序串列的時候,我們可以選擇要「遞增地」排序或者「遞減地」排序,這可以透過傳入的引數(Arguments)調整函數的參數(Parameters),具體來說,函數在定義的階段需要使用的資料型別或資料結構稱為參數,在使用的階段需要指定給參數的資料型別或資料結構就稱為引數。

def function_name(parameter_1, parameter_2):
    # body of the function
    # ...
    # ...sequence of statements

function_name(argument_1, argument_2)

通常參數會給予預設值,例如 sorted() 函數與串列的 sort() 方法都具有 reverse 參數,預設值給定為 True。所以在沒有傳入引數的狀況下,其實就是採用 reverse=False 的參數設定,假使不想採用預設值,就能選擇以名稱(Named)參數來傳入引數。

primes_list = [11, 7, 5, 3, 2]
sorted(primes_list)
sorted(primes_list, reverse=True) # Named
primes_list.sort()
primes_list.sort(reverse=True) # Named

更動結果的機制

除了使用「函數」或「方法」的差異,另外值得注意之處在於函數與方法的使用都可能讓應用或所屬的物件造成更動,一種更動方式是以回傳值型態輸出更動後的結果;另一種更動方式是直接更動資料型別與資料結構而沒有輸出。這個差異能夠延續前述內建函數 sorted() 與串列方法 sort() 的例子,sorted(a_list_to_be_sorted) 是以回傳值輸出排序後的串列,因此如果沒有將回傳值更新原本命名的串列,排序的更動並不會被保留。

primes_list = [11, 7, 5, 3, 2]
sorted(primes_list) # Apply sorted function to primes_list print(primes_list)  # primes_list is not sorted 
primes_list = sorted(primes_list)  # Update primes_list with function output
print(primes_list)  # primes_list is sorted

而 a_list_to_be_sorted.sort() 則是直接將排序更動了,不需要更新原本命名的串列,也不會伴隨有回傳值。

primes_list = [11, 7, 5, 3, 2]
primes_list.sort()  # Call sort method of primes_list print(primes_list)  # primes_list is sorted

多數情況下函數與方法會在「以回傳值輸出」或「直接更動物件的狀態」擇一,少數情況下會有兩者兼具的情況,例如串列方法 pop() 能夠將串列末端的資料拋出為回傳值,並刪除串列末端的資料點。

primes_list = [11, 7, 5, 3, 2]
the_last_element = primes_list.pop()
print(primes_list)       # The last element was deleted print(the_last_element)  # The last element was returned

這意味著設計為「函數」或者「物件的方法」並不代表恰好對應「以回傳值輸出」或「直接更動物件狀態」,絕大多數的函數確實是以回傳值輸出,但物件的方法則可能有以回傳值輸出的設計、有直接更動物件狀態的設計甚至是用參數來決定要回傳值輸出或直接更動物件狀態。例如資料科學模組 Pandas 所創造的 DataFrame 類別的 drop 方法就是採用 inplace 參數來決定「以回傳值輸出」或「直接更動物件狀態」。

import pandas as pd

df = pd.DataFrame()
df["order"] = list(range(1, 6))
df["prime"] = [2, 3, 5, 7, 11]
df.drop(axis=1, labels="prime")
print(df) # Column is not dropped
df.drop(axis=1, labels="prime", inplace=True)
df # Column was dropped

函數的來源

Python 使用者除了能夠應用內建函數以外,還可以從其他三個管道取得數值計算、文字處理、資料結構或更多其他應用所需的函數:

  1. 標準模組。

  2. 第三方模組。

  3. 自行定義。

僅有內建函數可以供我們直接使用,另外這三個管道的函數都必須在使用之前確定好在欲產生作用的範疇(作用域)中已經被載入或者定義。使用標準模組以及第三方模組中的函數之前,我們必須確認模組是否已經安裝載入,使用自行定義的函數之前,則是必須確認該函數是否已經完成定義。

載入模組的保留字是 import,使用模組的函數則需要在其名稱之前註明其模組名稱,例如引入標準模組 random,然後使用其中的 randint 函數隨機在 0 與 1 之間挑選整數。

import random 

print(type(random))
print(type(random.randint))
random_integer = random.randint(0, 1)
print(random_integer)

自行定義函數

Python 提供了三種「組織程式碼」的機制供使用者將未來想要重複使用的程式碼包裝成容易使用、容易擴充功能的形式,這三種機制會視應用規模由小到大區分為:

  1. 自行定義函數。

  2. 自行定義類別。

  3. 自行定義模組。

在這篇文章中我們專注自行定義函數,也就是最小的組織程式碼機制,在自行定義函數時候需要考慮五個組成要件:

  1. 函數名稱。

  2. 輸入。

  3. 參數。

  4. 函數主體。

  5. 輸出。

看似抽象,不過想想平日購買珍珠鮮奶茶的流程,就像是函數的運作一般:

  1. 函數名稱:購買珍珠鮮奶茶。

  2. 輸入:中杯 35 元;大杯 50 元。

  3. 參數:甜度(無糖、微糖、半糖、少糖、全糖)與冰塊(去冰、少冰、全冰)。

  4. 函數主體:點餐、貼標籤、加珍珠、加冰塊、倒茶、加鮮奶一直到封口。

  5. 輸出:珍珠鮮奶茶。

def function_name(INPUTS, PARAMETERS):
    # body of the function
    # ...
    # ...sequence of statements
    return OUTPUTS

作用域

約維安計畫:Python 的流程控制我們提到了程式區塊(Code blocks)的觀念,不論是條件判斷、迴圈或者例外處理,都仰賴縮排(Indentations)建立出程式區塊,並以此作為敘述的附屬。在自行定義函數時也同樣以程式區塊建構出函數主體,不過更特別的是,附屬於 def 敘述的程式區塊具有一個稱為作用域(或稱範疇 Scope)的觀念,在附屬於函數下的程式區塊被稱為 Local scope,函數以外的部分則稱為 Global scope。在 Local scope 中我們可以運用 INPUTS、PARAMETERS 來進行運算並宣告新的物件,但是這都侷限於 Local scope 之中,一但在 Global scope 意圖運用前述的這些物件,都會遭遇到 NameError,意即在 Global scope 中這些物件「沒有作用」,這也是作用域一詞的由來。例如在 Global scope 中試圖印出任何在 Local scope 中可以運用的物件都會遭遇 NameError。

def is_factor(a, b):
    modulo = a % b
    out = modulo == 0
    return out

print(is_factor(4, 2))
try:
    print(a)
    print(b)
    print(modulo)
    print(out)
except NameError:
    print("NameError occurred.")

return 的作用

初次學習自行定義函數多半都不甚暸解 return 保留字的作用,原因是先前太過頻繁地使用 print() 函數將文字處理、數值計算或者資料結構操作的結果顯示出來,導致在自行定義函數時不習慣去使用 return。事實上,return 保留字具有兩個作用,第一個跟字面上意義相同的將函數結果「輸出」,假如在自行定義函數時只有將運算結果用 print() 函數顯示出來,該函數就不具備能將結果儲存在物件中的能力,變成只能夠在使用時顯示結果,卻不能將結果儲存起來,例如想要將判斷因數的結果儲存起來,卻得到了 None

def is_factor(a, b):
    modulo = a % b
    out = modulo == 0
    print(out)

function_output = is_factor(4, 2)
print(function_output)

第二個是終止程式區塊的執行,在函數程式區塊中寫在 return 之後的所有內容在使用函數的時候都不會有任何作用。

def is_factor(a, b):
    modulo = a % b
    out = modulo == 0
    return out
    print(out)    # will not be printed
    print(modulo) # will not be printed
    print(a)      # will not be printed
    print(b)      # will not be printed

function_output = is_factor(4, 2)
print(function_output)

輸入與輸出的對應關係

自行定義函數的重點在於規劃輸入與輸出的對應關係,如同在數學課中學過的「函數映射關係」,只要釐清了自行定義函數針對輸入與輸出的設計機制,剩下的任務就僅是將程式區塊的函數主體完成而已。

從輸入開始,函數可以接受沒有輸入與參數的設計,在小括號中留空即可。

def hello_world():
    return "Hello, World!"

print(hello_world())

函數當然可以接受有輸入與參數的設計,好的函數設計會在參數給予合適的預設值。

def hello_someone(someone="World"):
    return f"Hello, {someone}!"

print(hello_someone())
print(hello_someone(someone="Function"))

設計要接受多個輸入值的函數時,可以用資料結構容納多個輸入資料。

def square_multiple_integers(x):
    out = []
    for elem in x:
        out.append(elem**2)
    return out

print(square_multiple_integers([2, 3]))
print(square_multiple_integers([2, 3, 5]))

如果希望多個輸入值分別以參數傳入而資料結構,可以運用 Python 自行定義函數的特殊設計:彈性參數(Flexible arguments),在小括號中給予 *args 就能夠在 Local scope 中以 tuple 處理 args

def square_multiple_integers(*args):
    print(type(args))
    out = []
    for elem in args:
        out.append(elem**2)
    return out

print(square_multiple_integers(2, 3))
print(square_multiple_integers(2, 3, 5))

再來是輸出,即便絕大多數的函數都具有回傳值,但也不代表函數非具備 return 不可,如果是這樣的情況我們會得到 None 為函數的回傳值。

def hello_world():
    out = "Hello, World!"

function_output = hello_world()
print(type(function_output))

多個輸出的處理預設是以 tuple 回傳,亦可以搭配 Python 的 Unpack 特性用多個物件去儲存。

def square_two_integers(a, b):
    out_a = a**2
    out_b = b**2
    return out_a, out_b

function_output_as_tuple = square_two_integers(3, 4)
func_out_a, func_out_b = square_two_integers(3, 4)
print(type(function_output_as_tuple))
print(func_out_a)
print(func_out_b)

多個輸出的處理相較輸入單純許多,就是採用適當的資料結構儲存。

def square_two_integers(a, b):
    out = {
        "a": a**2,
        "b": b**2
    }
    return out
function_output_as_dict = square_two_integers(3, 4)
print(function_output_as_dict)

在認識了函數的各種眉眉角角之後,第十六週約維安計畫:Python 的函數來到尾聲,希望您也和我一樣期待下一篇文章。

延伸閱讀

計畫學員專區

約維安計畫學員可以點選 nbgitpuller 連結將本篇文章完整的 Jupyter Notebook 複製到自己的 JupyterHub 之中:

nbgitpuller 連結

伺服器選項選擇預設的「Data science environment:學習 Python、R 與 Julia 的資料科學環境」就可以順利運作文章中的範例程式。


對於這篇文章有什麼想法呢?喜歡😻、留言🙋‍♂️或者分享🙌

Leave a comment