Python 程式設計

第 9 章    函式

(1) 函式

∗ 函式 (Function) :具有名稱的一群指令

▸ 函式的目的:

✶ 提供其他程式多次呼叫 (Function call),以避免重複撰寫程式碼
--> DRY (Don’t repeat yourself)
✶ 將相關的程式指令組在一起,讓整體程式更為模組化 (Modularization),更符合我們了解問題解決的方式
# 例如薪資系統功能可分為以下 4 個函式:
function.png
* 分成 4 個小函式,個別的問題較為獨立且單純,更易於解決

▸ 函式的語法:

def <funcName>(<parameters>):
    <body>
def:函式定義 (define),是複合指令,因此需以冒號結尾,且其本體指令群需縮排一層
<funcName>:函式的名稱 (不可使用 Python 保留字,且需符合 Python 識別字格式的規定)
<parameters>:函式的參數 (0 或多個)
<body>:函式的本體指令群

▸ Python 程式的撰寫風格:

✶ 函式前後均空 2 行
✶ 主程式裡或函式本體中,為區別功能可空 1 行

∗ 函式的參數

▸ 函式可接受參數,並由呼叫程式來提供,例如:

✶ 函式定義:
def printSum(x, y):
    print(f'兩個數值的和是:{x+y}')
# xy 是函式所接收的參數,亦稱為「正式參數」(Formal parameter )
✶ 呼叫程式:
a = 30
b = 50
printSum(a, b)
# ab 是呼叫函式時所提供的參數,亦稱為「實際參數」(Actual parameter)

▸ 呼叫程式可給不同的參數來多次呼叫函式,以產生不同的結果,如此可避免重複撰寫類似的程式,讓程式效能更高,例如:

a = 30
b = 50
printSum(a+3, b-5)    # 結果 78
printSum(10, 20)      # 30
printSum(a/3, b/5)    # 20.0

∗ 實際參數與正式參數的對應方式

▸ 位置參數 (Positional parameter):以位置來相互對應,例如:

✶ 定義:def fun(a, b, c):
✶ 以位置對應呼叫:fun(1, 2, 3)
--> a = 1, b = 2, c = 3

▸ 名稱參數 (Named parameter) 或鍵值參數 (Keyword parameter):以名稱來相互對應,例如:

✶ 定義:def fun(a, b, c):
✶ 以名稱對應呼叫:fun(b=3, c=4, a=5)
--> a = 5, b = 3, c = 4

▸ 預設正式參數值,該參數可以在呼叫時省略,例如:

✶ 定義:def fun(a, b, c=5):
✶ 以位置對應呼叫,並省略預設參數:fun(2, 3)
a = 2, b = 3, c = 5
✶ 無預設值之參數必須放在有預設值之參數之前,例如:
def fun(a, b=4, c):
--> SyntaxError: non-default argument follows default argument

▸ 練習 9-1

(2) 小烏龜的畫正方形函式

▸ 範例 1:傳入顏色及尺寸,函式繪製正方形

✶ 利用 turtle 模組來畫正方形時,我們希望寫相同一段程式來畫 不同尺寸 的正方形, 而不是針對每種尺寸的正方形都要寫一份程式 (否則就要寫無限個程式了!)
ch9/draw2Squares.py (首先建立 python/ch9 目錄)
import turtle


def drawSquare(t, color, size):
    t.color(color)         # 烏龜顏色
    for i in range(4):
        t.forward(size)    # 方形邊長 
        t.left(90)         # 左轉 90 度


screen = turtle.Screen()
screen.setup(700, 500)
screen.bgcolor('lightyellow')

myTurtle = turtle.Turtle()
myTurtle.shape('blank')    # 烏龜沒有形狀
drawSquare(myTurtle, 'blue', 100)  # 呼叫函式,給一組參數

myTurtle.left(180)
myTurtle.penup()
myTurtle.forward(100)
myTurtle.pendown()
drawSquare(myTurtle, 'red', 200)  # 呼叫函式,給另一組參數

screen.exitonclick()
drawSquare() 函式:
# 其本體指令群縮排一層
# 正式參數:t, color, step (烏龜物件、筆色、及步數),由呼叫程式傳入
# t.color(color):設定烏龜筆色
# for i in range(4):總共執行 4 次之迴圈
* 其本體指令縮排一層
* t.forward(size):往前走 size 距離
* t.left(90):左轉 90 度
✶ 呼叫程式:
# 從 screen = turtle.Screen() 指令開始均無縮排,因此從這個指令以下都不屬於 drawSquare() 函式的本體
# 第一次呼叫 drawSquare() 函式,畫一個藍色、邊長為 100 的正方形
# 小烏龜移到另一個位置
# 第二次呼叫 drawSquare() 函式,畫一個紅色、邊長為 200 的正方形

▸ 範例 2:以旋轉方式畫 20 個尺寸漸大的彩色正方形

ch9/draw20Squares.py
import turtle

def drawColorSquare(t, size):
    for color in ['red', 'blue', 'orange', 'green']:
        t.color(color)
        t.forward(size)
        t.left(90)

screen = turtle.Screen()               # 設定螢幕與特性
screen.setup(800, 600)

myTurtle = turtle.Turtle()             # 產生小烏龜
myTurtle.pensize(2)
myTurtle.speed(10)

size = 20                              # 最小正方形的尺寸
for i in range(20):
    drawColorSquare(myTurtle, size)
    size += 10                         # 每次增加方形尺寸
    myTurtle.forward(10)               # 往前走 10 步
    myTurtle.right(18)                 # 右轉 10 度

screen.exitonclick()
drawColorSquare() 函式:依照尺寸參數繪製彩色方形
✶ 呼叫程式:
# 每次呼叫函式後均加長方形邊長且移動加旋轉

▸ 練習 9-2

(3) 函式的回覆值與執行流程

∗ 回覆值的函式:函式在執行完畢會回覆值給呼叫程式

▸ 語法:在函式最後加上 return 指令,後面接一個或多個值或表示式

def <funcName>(<parameters>):
    ...
    return <values>

∗ 函式的執行流程 (Execution flow)

▸ 範例:計算數值的平方

ch9/square.py
def square(x):
    y = x*x
    return y


num = 5
result = square(num)
print(result)

▸ 程式的執行:

✶ Python 首先執行第 1 行指令:def square,Python 知道是要定義一個函式, 並且登記函式名稱為 square,如果以後遇到 square,Python 就會認得此名稱
# 第 2 ~ 3 行是函式本體指令群,這些指令並不會執行,函式需要透過呼叫才會執行,因此執行流程跳過這些指令
✶ 接著執行第 6 行指令:num = 5
✶ 接著執行第 7 行指令:result = square(num),此指令呼叫函式並將執行流程控制 (Flow control) 交給 square() 函式,從第 1 行開始執行
✶ 接著執行第 2, 3 行函式本體指令,return 指令不僅回覆一個值, 同時也將流程控制交還給主程式,回到第 7 行指令將回覆值指派給 result 變數
✶ 接著執行第 8 行指令,最後結束

▸ 因此,程式執行的流程為行 1 --> 6 --> 7 --> 1 --> 2 --> 3 --> 7 --> 8

▸ 程式定義要在呼叫程式之前,如此 Python 才會認得,因此以下程式會出現錯誤:

ch9/square.py
num = 5
result = square(num)
print(result)

def square(x):
    y = x*x
    return y
Traceback (most recent call last): 
  File "square.py", line 2, in <module> 
    result = square(num) 
NameError: name 'square' is not defined
--> square 名稱未定義, Python 不認得

▸ 如果一個函式沒有寫明回覆值,那麼預設回覆值就是 None ,例如:

ch9/square.py
def square(x):
    y = x*x
    # return y 

num = 5
result = square(num)
print(result)
None

(4) 函式的參數與變數

∗ 函式的參數與函式內的變數都是區域變數

▸ 在上例的函式中,有參數 x 及變數 y, 這兩者都只能在函式內部使用,在函式外面就無法使用,因此稱為區域變數 (Local variable),例如:

ch9/square.py
def square(x):
    y = x*x
    return y

num = 5
result = square(num)
print(result)
print(x)
Traceback (most recent call last): 
  File "square.py", line 9, in <module> 
    print(y) 
NameError: name 'x' is not defined
--> x 名稱未定義, Python 不認得 (若將 x 改為 y 後,同樣錯誤也會發生:Python 也不認得)
# 註:在函式中,在指派指令左方使用變數名稱, Python 就會產生一個區域變數

▸ 結論:

✶ 區域變數只能在其所定義的區域中使用,在該區域以外就無法使用
✶ 區域變數的壽命:函式開始執行時開始生命,函式執行完畢就結束

∗ 全域變數 (Global variable)

▸ 變數在所有地方都可使用

▸ 例如:

ch9/badSquare.py
def square(x):
    y = x**power
    return y


power = 2
result = square(10)
print(result)
power 定義在主程式,屬全域變數,在函式中亦可使用

▸ 注意:在函式裡使用全域變數是很 不好的作法,應該避免

▸ 解決方案:在函式中絕對不要使用全域變數,應該以參數方式將值傳給函式,例如:

ch9/goodSquare.py
def square(x, power):
    y = x**power
    return y


power = 2
result = square(10, power)
print(result)

▸ 當區域變數名稱與全域變數名稱相同,區域變數會遮蓋 (Shadow) 全域變數,例如:

ch9/shadowSquare.py
def square(x):
    power = 4 
    y = x**power
    return y


power = 2
result = square(10)
print(result)
print(power)
10000
2
✶ 在函式內的指派指令 power = 4 會產生新的區域變數,不會使用全域變數 (全域變數遭到遮蓋) ,因此有兩個 power 變數同時存在
✶ 最後一行指令會印出 2 ,顯示全域變數 power 的值並未被修改

(5) 函式呼叫其他函式

∗ 功能分解 (Functional decomposition)

像電腦科學家一樣思考:電腦科學家常常將一個大問題分解成為一群小問題,小問題比較容易解決,當每個小問題都解決,大問題就解決了

▸ 上述的程序稱為功能分解 (Functional decomposition):將一個系統分解為許多小功能,小功能就以函式的方式來解決,因此,整個系統是由許多函式組成

∗ 範例:利用 2 個函式來計算一串數值的平方和

▸ 在 sumOfSquares() 函式中呼叫 square() 函式以計算各個參數的平方,最後再加總

ch9/sumOfSquares.py
def square(x):
    y = x*x
    return y


def sumOfSquares(x, y, z):
    a = square(x)
    b = square(y)
    c = square(z)
    return a + b + c


a, b, c = -5, 2, 3
result = sumOfSquares(a, b, c)
print(result)
38

(6) 布林函式

∗ 布林函式 (Boolean function):回覆布林值的函式

▸ 許多的程式會利用布林函式來隱藏一些複雜的判斷,這對於程式的架構的清楚以及程式的可讀性都很有幫助

∗ 範例:判斷是否可整除的運算

▸ 不使用函式:

ch9/isDivisible.py
x, y = 10, 3
if x % y:
    result = False
else:
    result = True

print(result)
False

▸ 使用函式:

ch9/isDivisible2.py
def isDivisible(x, y):
    if x % y:
        return False
    return True


x, y = 10, 3
result = isDivisible(x, y)
print(result)
False

▸ 註:

✶ 習慣上會將布林函式命名為看起來像是需要回答是或否的問題,尤其是以 is 開頭, 例如:is divisible? yes/no --> 函式名稱:isDivisible()
✶ 在函式中,如果一個 if 判斷式最後會執行 return 指令, 那麼就不需要寫 else 指令,例如以上的 isDivisible() 函式,不要如下撰寫,以減少縮排層次:
if x % y:
    return False
else:
    return True
✶ 更簡潔的函式版本以及常見的使用方式:決策結構
def isDivisible(x, y):
    return x % y == 0


x, y = 10, 3
if isDivisible(x, y):
    ...    # Do something...
else:
    ...    # Do something else ...
--> 主程式看起來清楚多了,程式的說明性更高
✶ 常見的錯誤使用方式:多餘判斷
if isDivisible(x, y) == True:
    ...

▸ 練習 9-3

(7) 使用主函式

∗ 主函式 (Main function)

▸ 至目前為止,我們的程式包含主程式及函式

▸ 將主程式轉為主函式是個不錯的作法,可以使我們的程式更模組化 (Modularization)

▸ 例如繪製簡單方形的程式:

ch9/drawSquare.py
import turtle


def drawSquare(t, step):
    for i in range(4):    
        t.forward(step)
        t.left(90)


screen = turtle.Screen()
screen.setup(400, 300)
screen.bgcolor('lightyellow')
myTurtle = turtle.Turtle()
drawSquare(myTurtle, 100)
screen.exitonclick()
✶ 可以將主程式改寫為 main() 函式,並在最後一行呼叫 main()
ch9/drawSquareMain.py
import turtle


def main():
    screen = turtle.Screen()
    screen.setup(400, 300)
    screen.bgcolor('lightyellow')
    myTurtle = turtle.Turtle()
    drawSquare(myTurtle, 100)
    screen.exitonclick()


def drawSquare(t, step):
    for i in range(4):
        t.forward(step)
        t.left(90)


main()
✶ 現在程式結構如下:
1. 匯入 turtle 模組
2. 定義主函式 main()
3. 定義 drawSquare() 函式
4. 呼叫主函式
✶ 最後才呼叫 main() 的效果:Python 已讀取所有函式定義,因此 drawSquare() 函式可以放在 main() 之後,Python 會認得

∗ 進一步模組化:漂亮的程式架構!

▸ 將程式分成模組,每個模組有其功能,例如以上程式可以規劃成五個模組:main(), createScreen(), createTurtle(), drawSquare(), exitScreen()

mainFuncrion2.png
ch9/drawSquareModule.py
import turtle


def main():
    screen = createScreen(400, 300, 'lightyellow')
    myTurtle = createTurtle()
    drawSquare(myTurtle, 100)
    exitOnClick(screen)


def createScreen(width, height, bgcolor):
    screen = turtle.Screen()
    screen.setup(width, height)
    screen.bgcolor(bgcolor)
    return screen


def createTurtle():
    return turtle.Turtle()


def drawSquare(t, step):
    for i in range(4):
        t.forward(step)
        t.left(90)


def exitOnClick(screen):
    screen.exitonclick()


main()
✶ 如此,主函式的內容變得非常清楚,就是 4 項工作:產生螢幕、產生小烏龜、畫方形、及結束
✶ 程式的可讀性更高了,如果不在乎個別函式的內容,甚至可以不需要了解 (有可能是別的工程師負責撰寫,有可能在別的檔案), 我們只要看主函式就知道這份程式的功能了

∗ 支援程式匯入而不立即執行

▸ 我們所寫的程式也可以提供他人匯入使用,別人使用我們的程式的方式是先匯入,也就是不立即執行, 然後在需要的時候再呼叫我們所寫的函式,就如同我們匯入其他模組一樣:先匯入,需要時再執行

▸ 但 drawSquareModule.py 程式最後一行是執行主函式,因此別人匯入時就會立即執行, 不符合匯入需求

▸ 解決方案:以條件判斷是否要執行 main() 函式

✶ Python 提供一個名為 __name__ 的內建變數,如果一個程式檔案被執行了, __name__ 的值就會被設定為 '__main__' 字串
✶ 因此,我們可以判斷 __name__ 的值,確定程式是被匯入還是被執行, 如果是被執行就呼叫 main() 函式,否則就不呼叫
✶ 修改程式:
ch9/drawSquareModule.py
...

def exitOnClick(screen):
    screen.exitonclick()


main()
if __name__ == '__main__':
    main()
# 最後兩行:判斷 __name__ 是否等於 __main__, 如果是,就呼叫 main() 函式
✶ 假設有另一個程式需要使用某個模組裡的某個函式或變數, 在該程式需要先匯入模組,然後再呼叫函式,呼叫語法為 <moduleName>.<functionName>()<moduleName>.<variableName>,例如:
ch9/callDrawSquareModule.py
import turtle
import drawSquareModule


def main():
    screen = turtle.Screen()
    screen.setup(400, 300)
    screen.bgcolor('lightgray')
    myTurtle = turtle.Turtle()
    myTurtle.color('red')
    drawSquareModule.drawSquare(myTurtle, 100)
    screen.exitonclick()


if __name__ == '__main__':
    main()
# 如此程式就更容易分享了
✶ 註:匯入一個模組只需要寫主檔名,不需加上副檔名 .py

上一章       下一章