Python 入門指南 5.0
單元 19 - 作用域問題
全域變數 (global variable) 是指定義在所有定義之外的變數 (variable) ,區域變數 (local variable) 則是定義在程式區塊 (block) 內的變數
這裡的程式區塊指的是關鍵字 (keyword) def 之下的縮排區域,所以是函數 (function) 或方法 (method) 的內容。
Python 中全域變數跟區域變數是涇渭分明的,舉一個簡單例子如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | # 定義全域變數 a
a = 1
# 定義函數
def do_something():
# 定義函數內的區域變數
a = 2
# 印出區域變數
print(a)
# 呼叫函數印出函數中定義的區域變數
do_something()
# 印出全域變數 a
print(a)
# 檔名: scope_demo01.py
# 說明:《Python入門指南》的範例程式
# 網站: http://kaiching.org
# 作者: 張凱慶
# 時間: 2023 年 5 月
|
第 2 行設定了全域變數 a ,然後設定為整數 1
2 | a = 1
|
在 do_something() 定義了相同名稱的區域變數 a ,並把區域變數 a 設定為整數 2 ,並且在 do_something() 的最後印出區域變數 a
4 6 8 | def do_something():
a = 2
print(a)
|
原本函數可以直接使用全域變數,但是這裡在函數內定義了跟全域變數相同名稱的區域變數,那函數內識別字 (identifier) a 跟函數外的識別字 a 無關,函數內區域變數 a 的效力範圍僅在函數中。
第 10 行,呼叫 do_something() 方法
10 | do_something()
|
第 12 行,印出全域變數 a
12 | print(a)
|
執行結果如下
$ python scope_demo01.py |
2 |
1 |
$ |
以上先印出整數 2 ,這是 do_something() 內的區域變數,然後印出整數 1 這是最後印出全域變數值。
如果想要在函數內修改區域變數,那就需要用關鍵字 global 宣告全域變數的識別字名稱,例如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | # 定義全域變數 a
a = 3
# 定義函數
def do_something2():
# 宣告全域變數 a
global a
# 印出全域變數 a
print(a)
# 重新設定全域變數
a = 4
# 呼叫函數
do_something2()
# 印出全域變數 a
print(a)
# 檔名: scope_demo02.py
# 說明:《Python入門指南》的範例程式
# 網站: http://kaiching.org
# 作者: 張凱慶
# 時間: 2023 年 5 月
|
這裡在 do_something2() 中做三件事,第一件事用關鍵字 global 宣告全域變數 a ,第二件事印出全域變數 a ,第三件事重新設定全域變數 a 為整數 4
4 6 8 10 | def do_something2():
global a
print(a)
a = 4
|
下面第 12 行呼叫函數 do_somehting2()
12 | do_something2()
|
然後第 14 行印出全域變數 a
14 | print(a)
|
執行結果如下
$ python scope_demo02.py |
3 |
4 |
$ |
因為 do_somehting2() 最後修改了全域變數 a 的值為整數 4 ,所以最後印出的就是整數 4 。
另外一種情況是在函數中,如果巢狀函數 (nested function) 想要修改上層函數的區域變數,就要用關鍵字 nonlocal 宣告。注意跟 global 一樣,必須先宣告才在巢狀函數中修改上層區域變數,舉例如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | # 定義全域變數 a
a = 5
# 定義函數
def do_something3():
# 定義 do_something3 的區域變數
a = 6
# 定義第一個巢狀函數
def do_something4():
# 宣告上層 do_something3 的區域變數
nonlocal a
# 印出上層區域變數
print(a)
# 重新設定上層區域變數
a = 7
# 定義第二個巢狀函數
def do_something5():
# 宣告全域變數
global a
# 印出全域變數
print(a)
# 定義第三個巢狀函數
def do_something6():
# 設定區域變數
a = 8
# 印出區域變數
print(a)
# 呼叫巢狀函數
do_something4()
do_something5()
do_something6()
# 印出 do_something3 的區域變數
print(a)
# 呼叫 do_something3() 函數
do_something3()
# 檔名: scope_demo03.py
# 說明:《Python入門指南》的範例程式
# 網站: http://kaiching.org
# 作者: 張凱慶
# 時間: 2023 年 5 月
|
這裡指函數,其實方法也適用,因為方法是類別專屬的函數。
第 2 行先定義全域變數,因此這個例子也會看到全域變數在巢狀函數中的情況
2 | a = 5
|
在 do_somehting3() 中先定義屬於 do_somehting3 的區域變數 a ,並設定成整數 6
4 6 | def do_something3():
a = 6
|
然後底下依次定義三個巢狀函數,先看到 do_something4()
8 10 12 14 | def do_something4():
nonlocal a
print(a)
a = 7
|
第 10 行用關鍵字 nonlocal 宣告上層區域變數 a
10 | nonlocal a
|
在 do_something4() 的後續是印出這個變數 a ,然後重新設定成整數 7
繼續看到第二個巢狀函數 do_something5() ,這裡宣告全域變數,然後印出全域變數
16 18 20 | def do_something5():
global a
print(a)
|
然後第三個巢狀函數 do_something6() ,設定屬於 do_something6 的區域變數 a ,然後印出 a
22 24 26 | def do_something6():
a = 8
print(a)
|
do_something3() 的最底下是依次呼叫三個巢狀函數,然後印出 do_somehting3 的區域變數 a
28 29 30 32 | do_something4()
do_something5()
do_something6()
print(a)
|
最底下執行部分則是呼叫 do_something3()
34 | do_something3()
|
執行結果如下
$ python scope_demo03.py |
6 |
5 |
8 |
7 |
$ |
這裡我們重新檢視結果,第一個印出的 6 是第 6 行的定義
6 | a = 6
|
第二個印出的 5 是第 2 行的定義
2 | a = 5
|
第三個印出的 8 是第 24 行的定義
24 | a = 8
|
第三個印出的 7 是第 14 行的定義
14 | a = 7
|
理解程式中的執行順序是很重要的課題,尤其寫出容易閱讀的程式碼也就意味著更好維護, Python 用文件字串 (documentation string) 的方式把程式文件直接插入到程式碼中,接下來我們來看看怎麼寫 Python 的程式文件。
中英文術語對照 | |
---|---|
程式區塊 | block |
文件字串 | documentation string |
函數 | function |
全域變數 | global variable |
識別字 | identifier |
關鍵字 | keyword |
區域變數 | local variable |
方法 | method |
巢狀函數 | nested function |
變數 | variable |
重點整理 |
---|
1. 全域變數是指定義在沒有縮排的變數,區域變數則是定義在有縮排的地方。 |
2. 關鍵字 global 可以在函數或方法修改全域變數。 |
3. 巢狀函數是指函數中定義其他函數。 |
4. 關鍵字 nonlocal 可以在巢狀函數中修改上層的區域變數。 |
問題與討論 |
---|
1. 全域變數跟區域變數有何不同? |
2. 函數中為什麼不能直接修改全域變數? |
3. 外層巢狀函數可以使用內層巢狀變數的值嗎? |
練習 |
---|
1. 寫一個程式 exercise1901.py ,裡面建立一個全域變數,例如 count ,然後定義一個函數印出這個全域變數,函數可命名為 print_count() ,最後執行部分就呼叫 print_count() 印出 count 值。 參考程式碼 |
2. 承上題,將新程式寫在 exercise1902.py 中,新定義一個函數 add_count() 遞增全域變數 count ,執行部分增加呼叫 add_count() ,然後再呼叫 print_count() 一次。 參考程式碼 |
3. 承上題,將新程式寫在 exercise1903.py 中,先引入 exercise1902 ,然後用 exercise1902 的 count 定義全域變數 count ,然後定義 add_count2() 將 count 加上 10 , print_count2() 印出 count ,執行部分分別呼叫 exercise1902 的 add_count() 及 add_count2() ,然後用 print_count2() 及 exercise1902 的 print_count() 印出兩個作用域不同的 count 值。 參考程式碼 |
4. 承上題,將新程式寫在 exercise1904.py 中,先引入 exercise1902 ,然後定義函數 global_variable() 以 exercise1902 的 count 建立全域變數 count 。 參考程式碼 |
5. 承接上一個單元的練習 2 ,將新程式寫在 exercise1905.py 中,將計算階乘的部分放到內層函數 calculate() 中。 參考程式碼 |
6. 承上題,將新程式寫在 exercise1906.py 中,將 calculate() 改成 recursion() ,改成用遞迴的方式計算階層。 參考程式碼 |
7. 承接上一個單元的練習 4 ,將新程式寫在 exercise1907.py 中,將計算費氏數列的部分放到內層函數 calculate() 中。 參考程式碼 |
8. 承上題,將新程式寫在 exercise1908.py 中,將 calculate() 改成 recursion() ,改成用遞迴的方式計算費氏數列。 參考程式碼 |
9. 寫一個新程式 exercise1909.py ,定義函數 exercise1909() ,裡面建立區域變數 data 並設定為 100 ,然後定義內層函數 inner() , inner() 用參數 number 跟 data 進行比較,如果 data 較大就回傳 data ,反之回傳 0 ,最後 exercise1909() 回傳 inner() 。 參考程式碼 |
10. 寫一個新程式 exercise1910.py ,定義函數 my_sum() ,裡面建立區域變數 data 並設定為空串列,然後定義內層函數 calculate() , calculate() 將參數 number 附加到 data 中,然後回傳 data 中的總和,最後 my_sum() 回傳內層函數 calculate() 。 參考程式碼 |