Learning Objectives
- 使用匿名函數(lambda),並把函數當作引數傳給另一個函數
- 命名空間(namespace)與變數的可見度(scope)
- 區域變數(local variables)的用法
- 全域變數(global variables)的用法與 global 關鍵字
這一課要回答的兩個問題
- 能不能不取名字,直接寫一個小函數? 有些函數只有一行、只用一次,例如「排序時依照分數」。
為它特地寫
def顯得冗長,這時可以用 lambda。 - 同一個名字,在函數裡和函數外是同一個變數嗎? 上一課看到函數裡的
myvar和外面的myvar各在自己的框架裡。這一課要正式說明 Python 怎麼決定「這個名字指的是哪一個變數」,也就是變數的可見度(scope)。
Lambda 運算
lambda 又稱為匿名(anonymous)函數:
lambda 參數1, 參數2, ...: 運算式
- 可以接收多個引數,但只能有一個運算式,運算式的結果就是傳回值(不必寫
return)。 - 語意上它只是正規函數定義的語法糖(syntactic sugar):功能一樣,寫法更簡潔。 這個詞由英國電腦科學家 Peter Landin 提出,指不影響語言功能、但讓程式設計師更方便的語法。
用變數參考 lambda 函數
第 2 行把一個 lambda 函數指派給 sum_data,之後就能像一般函數一樣呼叫。播放時注意:呼叫 lambda 時,右邊也會出現一個框架,名稱顯示為 <lambda>,因為它本身沒有名字。
把函數當作引數
在 Python 裡,函數也是物件,可以像數字、字串一樣當作引數傳遞。operate_on 不知道也不在乎 func 是哪個函數,只負責呼叫它。看第 9 行傳入有名字的 sum_data、第 11 行直接傳入 lambda。
lambda 當作 sorted() 與 max() 的 key
key 參數接收一個函數,sorted() 會對每個元素呼叫它,再依照傳回的結果排序。數數看第 3 行的 lambda 被呼叫了幾次:每個學生一次。
想一想:lambda 與函數引數
下面的程式會印出什麼?
f = lambda x, y: x * y + 1
print(f(3, 4))
下面的程式會印出什麼?
def apply(func, v):
return func(func(v))
print(apply(lambda n: n * 2, 3))
下面的程式會印出什麼?
words = ["pear", "fig", "banana"]
print(sorted(words, key=lambda w: len(w)))
命名空間與變數的可見度
變數是對應到物件的名稱。Python 把「名稱與物件的對應」存放在一種資料結構裡,稱為命名空間(namespace)。
- 每個函數被呼叫時,都有自己的區域命名空間;在函數之外的名稱,放在模組的全域命名空間。
- 在函數內被指派的變數是區域變數,只能在那個函數裡存取,函數結束就消失。
- 在函數外的變數是全域變數,整個程式(包含所有函數)都可以讀取。
- Python 沒有「宣告變數」這件事(沒有
int x = 5這種寫法)。只要函數裡有指派給某個名稱,它就自動被視為區域變數, 除非用global明確宣告。 - 區域變數與全域變數同名時,函數裡以區域變數優先。
視覺化工具右邊的每一個框架,就是一個命名空間:最下面的模組框架是全域,上面的函數框架是區域。
區域變數蓋掉同名的全域變數
全域和 sum_data 裡都有一個 total。看第 5 行執行後:新的 total 出現在 sum_data 的框架,模組框架裡的 total 仍然是 0。函數結束後,區域的 total 跟著框架一起消失。
global 關鍵字
三個函數對 eggs 做了三件不同的事:ham() 只讀取、bacon() 指派(建立區域變數)、spam() 先宣告 global 再指派(修改全域變數)。預測一下三個 print 各印出什麼,再播放驗證。
LEGB:名稱的查找順序
函數裡還可以再定義函數。這時候 Python 找一個名稱的順序是由內往外的 LEGB:
| 順序 | 範圍 | 說明 |
|---|---|---|
| L | Local | 目前這個函數的區域 |
| E | Enclosing | 外層函數的區域(函數裡定義函數時才有) |
| G | Global | 模組的全域 |
| B | Built-in | Python 內建的名稱,例如 print、len、range |
找到就停;四層都找不到,就會引發 NameError。
巢狀函數與 Enclosing
播放到第 8 行時,右邊會有三個框架:模組、outer、inner。inner 的框架裡只有 y,所以 x 要往外層 outer 找,印出的是 enclosing,而不是 global。
追蹤變數的可見度
下面的程式會印出什麼?
n = 1
def f():
n = 5
return n
print(f(), n)
下面的程式會印出什麼?
count = 0
def add():
global count
count += 1
add()
add()
print(count)
把上一題的 global count 這一行刪掉,執行結果是什麼?
count = 0
def add():
count += 1
add()
print(count)
實作:排名與計數器
完成兩個 TODO:(1) 用 sorted() 搭配 lambda,把 students 依分數由高到低排序;(2) 讓 record() 每被呼叫一次,全域變數 calls 就加 1。
重點精華回顧
lambda 參數: 運算式建立匿名函數;只能有一個運算式,結果就是傳回值,是正規函數的語法糖。- 函數是物件,可以指派給變數、當作引數傳給別的函數;最常見的是
sorted()、max()的key。 - 命名空間存放名稱與物件的對應;每次函數呼叫都有自己的區域命名空間。
- 函數裡被指派的名稱是區域變數,只在函數裡有效;函數外的是全域變數,函數裡可以直接讀取。
- 要在函數裡修改全域變數,必須先宣告
global 名稱;否則會建立同名的區域變數,或引發 UnboundLocalError。 - 名稱查找順序 LEGB:Local → Enclosing → Global → Built-in,找到就停。