【Python基礎教學】lambda & 裝飾器 & 命名空間及作用域【part-12】
【Python基礎教學】lambda & 裝飾器 & 命名空間及作用域【part-12】
哈囉大家好,很感謝你點進本文章,我是一名高中生,是電腦社社團的社長,由於我並不是 Python 這方面非常精通的專家,所以若文章有些錯誤請見諒,也可向我指正錯誤。另外本文章的用意是作為電腦社社團的教材使用而編寫的。
上次我們談完了遞迴,也介紹比列表更快的資料結構:陣列(array),陣列結束後緊接著是類別(class)的介紹,在類別中,又有項功能叫做繼承,繼承不只單一繼承,也能夠多重繼承。今天將上一篇所挖的坑來填補一下(命名空間),也來額外教給各位匿名函數 lambda、裝飾器(decorator)的使用教學。
接下來,讓我們進入正題(本篇長文請注意!)。
lambda(匿名函數)

Image Source:Mastering Lambda Expressions in Python: A Hands-On Guide | by John Vastola | Level Up Coding
lambda 函式是「只有一行」的函式,可以用來處理一些小型函式,就可以不用為了一小段程式碼,額外新增一個有名稱的函式。
在 Python 中,我們使用 lambda 關鍵字來建立匿名函數,而 lambda 會叫做匿名函數是因為他是一個沒有名稱的函數。
lambda 函數是一種小型、匿名的、內建函數,它可以有任意數量的參數,但只能有一個運算式。匿名函數不需要使用 def 關鍵字定義完整函數。
以下是 lambda 的特點:
- 匿名函式不需要定義名稱,一般函式需定義名稱。
- 匿名函式只能有一行運算式,一般函式可以有多行運算式。
- 匿名函式執行完成後自動回傳結果,一般函式加上 return 關鍵字才能回傳結果。
lambda 語法格式:
1 | lambda arguments: expression |
- lambda:Python 的關鍵字,用來定義 lambda 函數。
- arguments:引數列表,可以包含零個或多個參數,但必須在冒號(:)前指定。
- expression:一個運算式,用於計算並回傳函數的結果。
以下的 lambda 函數沒有參數:
1 | f = lambda: "Hello, world!" |
如果我們將它寫成一般的函數形式,會長這樣:
1 | def f(): |
以下範例中,我們使用 lambda 建立匿名函數,設定一個函數參數 a,函數計算參數 a * a 的平方數,並回傳結果:
1 | x = lambda a : a * a |
同樣地,如果我們將它寫成一般函數的形式,會長這樣:
1 | def x(a): |
lambda 可以使用多種參數,如下所示:
1 | x = lambda a, b : a * b |
如果我們寫成一般的函數形式:
1 | def x(a, b): |
lambda 函數通常與內建函數如 map()、filter() 和 reduce() 一起使用,以便在集合上執行操作。例如:
1 | numbers = [1, 2, 3, 4, 5] |
map() 函數解釋:
:::success
map() 會根據提供的函數對指定序列做映射。
第一個參數 function 以參數序列中的每一個元素呼叫 function 函數,回傳包含每次 function 函數傳回值的新列表。
:::
簡單來說,假設可迭代的序列是一個列表,那麼這個列表裡面所有元素都會被當成是 function 的參數,之後作為引數引入函數當中執行。執行完成後會回傳值,回傳的資料型態會是一個列表值。
以下是 map() 的語法:
1 | map(function, iterable, ...) |
參數:
- function -> 函數
- iterable -> 一個或多個可迭代的序列
以下是使用 map() 函數的範例:
1 | a = ['1','2','3'] |
如果我們不加上 list 強制轉換成 list 資料型態,會變成這樣:
1 | a = ['1','2','3'] |
所以我們記得要替他加上資料型態做顯式轉換,不然印出來的只是 map 物件所在記憶體的位址。
我們回到這個範例:
1 | numbers = [1, 2, 3, 4, 5] |
首先我們知道 map() 函數第一個引數叫做函數,所以在範例中就使用了 lambda 作為函數使用。這個 lambda 函數設置一個參數名為 x,且回傳值為 x 的平方數,而 map() 函數第二個引數叫做可迭代的序列,叫做 numbers。
好的,相信各位都知道接下來會發生什麼了,沒錯,numbers 內的元素會一個一個當作 lambda 的參數進入 lambda 函數裡面進行運算,接下來靠 map()、list() 將回傳值輸出成列表,就能夠產出每一個元素的平方數囉。
接下來我們使用 lambda 函數與 filter() 一起篩選偶數:
1 | numbers = [1, 2, 3, 4, 5, 6, 7, 8] |
filter() 解釋:
:::success
filter() 函數用來過濾序列,過濾掉不符合條件的元素,回傳由符合條件元素組成的新列表。
此接收兩個參數:第一個為函數,第二個為序列,序列的每個元素作為參數傳遞給函數進行判斷,然後回傳 True 或 False,最後將回傳 True 的元素放到新列表中。
:::
filter() 在引數的用法上與 map() 是相同的(前面函數,後面放引數),filter() 就如他英文上字面意思一樣,是用來篩選過濾掉不合條件的元素,以下是一個範例,或許看了之後你會比較明白(範例來自菜鳥教程):
1 | def is_odd(n): |
首先第一行的函數,設有參數 n,並回傳若 n % 2 == 1 時,回傳 True,否則 False。
而 filter 函數就是利用後面的序列內的元素一個一個代入(迭代列表)函數裡面,檢查是否為 True,如果是 True 的話,那麼就將這個元素給保留下來。(運作方式與 map() 相同,但是功能不同。)
與 map() 函數不同的是,map() 函數並沒有做檢查 True、False 的動作,而是直接將函數的回傳值替代列表中的那個元素;filter() 函數需要做 True、False 的檢查,然後才能將函數的回傳值替代列表中的那個元素。
我們需要注意的是,filter() 函數如果要變成列表的話,也跟 map() 一樣要加上 list() 表示。
我們繼續回到上面的範例中:
1 | numbers = [1, 2, 3, 4, 5, 6, 7, 8] |
經過 filter() 的解釋過後,相信各位就能很快應用到 lambda 上面囉。剛剛的範例是奇數,這次是偶數,比較 x % 2 是否等於 0,是的話回傳 True,否則 False。
順帶一提,以下是使用迴圈的寫法:
1 | list1 = [] |
輸出結果:
1 | 2 |
大家可以思考看看,如果我們的資料有十萬個資料需要處理,哪一種方式會是比較有效率的呢?順帶一提,可以往程式碼可讀性的方向去取捨跟思考。
接下來是使用 reduce() 函數搭配 lambda 的寫法(範例來自菜鳥教程):
1 | from functools import reduce |
reduce() 解釋:
:::success
reduce() 函數會對參數序列中元素進行累積。
函數將一個資料集合(鏈結串列,元組等)中的所有資料進行下列操作:用傳給 reduce 中的函數function(有兩個參數)先對集合中的第 1、2 個元素進行操作,得到的結果再與第三個資料用function 函數運算,最後得到一個結果。
:::
註:reduce() 在 python 3.x 版本以上已經被移除全域命名空間(Global NameSpace)了。現在它被放在模組 functools 裡面,需要額外引入模組。
reduce() 函數語法:
1 | reduce(function, iterable[, initializer]) |
- function — 函數,有兩個參數
- iterable — 可迭代對象
- initializer — 可選(optional),初始參數
reduce() 參數上基本和 map()、filter() 相同,多了一個參數 initializer 可選而已。
同樣地,以下是使用 reduce() 的範例(來自菜鳥教程):
1 | from functools import reduce |
除了引入模組外,我們可以直接用 lambda 函數濃縮成一行:
1 | from functools import reduce |
以上範例,可以看到 reduce() 函數的功能就是將所有元素代入(迭代列表)add 函數的參數裡面,且依據函數內的運算來對所有元素進行運算,上面的範例是將所有元素相加,也就是計算全部數字的總和。
必須注意的是,函數的參數必須要有兩個,多一個少一個都不行,否則會報錯。
回到 lambda 運用 reduce() 的範例,我們同樣可以用它直接濃縮成一行:
1 | from functools import reduce |
lambda 其他範例
以下範例均來自(匿名函式 lambda - Python 教學 | STEAM 教育學習網):
1 | def x(n): |
註:x 函數等於 y。
x 函數是使用 range() 函數來創建列表的,而 y 則是使用 lambda 設置參數,回傳值使用列表生成式。
1 | def y(n): |
註:y 函數等於 x。
另外給各位同學補充,也是作者我本人推薦的一個工程師所寫的文章:Lambda — Python 重要語法技巧 | JohnLiu 的軟體工程思維
裡面談及到了 sorted 在 lambda 上的應用,程式碼都講解的十分清楚,另外也將 lambda 運用到方法中,總之推爆。
裝飾器(Decorators)
裝飾器 decorator 是 Python 的一種程式設計模式,裝飾器本質上是一個 Python 函式或類 ( class ),它可以讓其他函式或類別,在不需要做任何代碼修改的前提下增加額外功能,裝飾器的回傳值也是一個函式或類對象,有了裝飾器,就可以抽離與函式功能本身無關的程式碼,放到裝飾器中並繼續重複使用。
裝飾器(decorators)是 Python 中的進階功能,它允許你動態地修改函數或類別的行為。
裝飾器是一種函數,它接受一個函數作為參數,並回傳一個新的函數或修改原來的函數。在 Python 中,使用「@」當做裝飾器使用的語法糖符號(語法糖指的是將複雜的程式碼包裝起來的糖衣,也就是簡化寫法)。
以下是裝飾器可應用的地方:
- 日誌記錄(就是你常見到的那個 log 文件):裝飾器可用於記錄函數的呼叫資訊、參數和回傳值。
- 效能分析(performance analysis):可以使用裝飾器來測量函數的執行時間。
- 權限控制(permission control):裝飾器可用來限制對某些函數的存取權限。
- 快取(cache):裝飾器可用於實現函數結果的快取,以提高效能。
製作第一個裝飾器
下方的程式碼,定義了一個裝飾器函式 a 和一個被裝飾的函式 b,當 b 函式執行後,會看見 a 運算後的結果,套用在 b 函式上。
1 | def a(func): |
範例來源自:裝飾器 decorator - Python 教學 | STEAM 教育學習網
相信各位最困惑的一定會是 b = a(b) 這一行吧?接下來就讓我來跟大家解釋解釋:
這行程式碼是呼叫了函數 a,並且將函數 b 作為它的參數傳入函數 a。而進入到函數 a 裡面後,執行 print(‘makeup…’),之後回傳函數 b,回傳的函數 b 被賦值給變數 b。
在 Python 中,我們可以將函式 function 可以當成參數傳遞並執行。
最後呼叫函數 b(),印出 go。

圖源:裝飾器 decorator - Python 教學 | STEAM 教育學習網
在這邊各位肯定也會有個疑問:為什麼不要先呼叫 a() 再呼叫 b()就好?
雖然打印出來的東西會一樣,但是你要知道函數 a 是一個裝飾器(Decorator),它的目的是在不修改函數 b 的情況下,給函數 b 新增一些新的功能,而函數 b 的新功能就是打印出 makeup…。
:::success
總之,裝飾器的目的就是給某函式增加功能,而這個功能的來源可能是別的函數,重點是它會在不修改「原有的」的函式之下增加它的功能。
:::
當我們寫 b = a(b) 時,其實是就在呼叫函數 a,並將函數 b 作為參數傳入。然後,函數 a 回傳了一個「新的函數」,這個新的函數被賦值給了 b 變數。所以,當我們再次呼叫 b() 時,我們其實是在呼叫這個新的函數。
如果我們先呼叫函數 a,然後再呼叫函數 b,那麼函數 b 就不會有新的功能,所以函數 b 就不會同時打印出 makeup… 跟 go。
所以這段程式碼的目的是在「不修改」原始函數 b 的情況下,給函數 b 新增新的功能。
發揮好學有學問的同學們,另外可以思考一下為什麼要寫成:b = a(b),而不是 b = a(b())?
提示:函數呼叫與參數傳遞之間的關係。
:::spoiler 點我看答案
函數本身 -> b
函數的呼叫 -> b()
因為函數 a 的參數是一個函數。
:::
1 | def a(func): |
最後補上裝飾器的定義:
1 |
|
等於:
1 | def target_function(): |
下面例子的 time_logger 是假設上面已經有一個已定義好的函數叫 time_logger,如同 steam 教育學習網的範例 a() 函數一樣。
多個裝飾器
如果有多個裝飾器,執行的順序將會「由下而上」觸發(函式一層層往上),下方的程式碼,會先裝飾 a3,接著裝飾 a2,最後裝飾 a1。
1 | def a1(func): |
引用自:裝飾器 decorator - Python 教學 | STEAM 教育學習網
多個參數處理
1 | def a(func): |
如果有多個參數需要處理,我們則會使用可變位置參數(*args)、可變關鍵字參數(**kwargs)來取得所有參數,如果忘記這兩個是啥的同學請回顧:點我傳送門
程式碼解析:
首先第一行定義了一個名為 a 的裝飾器,接收一個函數 func 作為參數。
在 a 函數內部,定義了一個新的函數 c,這個函數可以接收任意數量的可變位置參數和可變關鍵字參數(args 和 *kwargs)。
函數 c 首先列印出接收到的可變位置參數和可變關鍵字參數,然後列印出 ‘ok…’,最後呼叫 func 函數並將 args 和 *kwargs 傳入 func 函數中。
之後 a 函數回傳 c 函數的內容物。
第九行使用 @a 裝飾 b 函數,等同於 b = a(b)。而當我們呼叫 b 函數時,實際上是呼叫 c 函數。
第十二行呼叫 b 函數,傳入一個列表 [123, 456] 和三個可變關鍵字參數 x=1, y=2, z=3。這些參數會被傳入 c 函數,並被列印出來。之後列印出 ‘ok…’,最後呼叫原始的 b 函數(即 func),並將這些參數傳入。
總之,看到這裡,如果你對裝飾器的觀念還不是很清楚的話,我們可以將裝飾器這個機制,把它想成是將某函數裡面的功能賦予給某某函數作使用。
類別裝飾器
除了函數裝飾器,Python 還支援類別裝飾器。類別裝飾器是包含
__call__方法的類,它接受一個函數作為參數,並回傳一個新的函數。
1 | class DecoratorClass: |
註:範例改自菜鳥教程
程式碼解析:
第一行首先定義類別名為 DecoratorClass,之後定義建構方法 __init__ 接收一參數名為 func,將其儲存到實例變數 self.func 裡面。
另外類別裡面定義了一個 __call__ 的方法,剛剛上面說過,這個方法主要能夠接受一個函數作為參數,並且回傳新的函數回去。
然後在這個方法裡面呢,會首先呼叫 self.func 函數,並將 args、*kwargs 傳入函數中,之後印出兩參數,最後回傳函數結果。
之後使用語法糖裝飾器 @DecoratorClass 裝飾 my_function()。
最後一行則呼叫 my_function 本身,且傳入參數 [123, 456, 789]、x=1, y= 2, z=3。
很神奇的是,換成類別裝飾器傳遞多參數之後,竟然會是 “Hello” 會先印出來欸,這跟之前那個多參數傳遞的例子不一樣欸?好吧,老實告訴你,其實這並不是換成什麼什麼裝飾器所改變的結果,差別就在於 __call__ 程式碼中”那個”的擺放順序,相信聰明的你馬上就看出來哪裡需要修改了。
小結
裝飾器能夠接收一個函數作為參數,裝飾器的回傳值也是一個函數或類別物件。
當某個函數加上裝飾器後,執行該函數之前會先執行裝飾器的內容,之後才會再執行最初呼叫的那個函數,也稱為原始函數。
裝飾器你也可以把它想成是有某函數給予某某函數它的功能。
當我們要在裝飾器使用多個參數時,可以使用 *args、*kwargs。
要在類別中使用裝飾器時,使用 __call__ 方法來進行傳遞函數跟回傳函數的動作。
命名空間(Namespace)
以下是 python 官方對於命名空間的一段話:
A namespace is a mapping from names to objects. Most namespaces are currently implemented as Python dictionaries。
翻譯過來就是:命名空間(Namespace)是從名稱到物件的映射,大部分的命名空間都是透過 Python 字典來實現的。
什麼意思呢?命名空間(namespace)是一種把名稱與物件連接起來的技術,在 Python 中,所有的名稱都在某種命名空間中。名稱與物件之間的映射就像一本字典,我們可以透過名稱找到對應的對象。
讓我們複習一下字典的鍵值對:一個鍵肯定是對上一個值嘛,像是 {‘x’ : 1}。映射的意思其實就是「對」,在英文中叫作 map,你也可以想成是 connect,一個鍵「對」上一個值,也可以解釋成一個鍵「連接或映射」一個值。
當我們談及到命名空間的概念時,一定會說明到一個觀念:全域變數(global variable)、局域變數(local variable)。
那什麼是全域變數跟局域變數呢?
在 Python 裡的主程式與每個函式,都有各自的名稱空間 ( namespace ),簡單的區分規則如下:
- 主程式定義「全域」的名稱空間,在主程式定義的變數是「全域變數」。
- 個別函式定義「區域」的名稱空間,個別函式裡定義的變數就是「區域變數」。
- 每個名稱空間裡的變數名稱都是「唯一的」。
- 不同名稱空間內的變數名稱可以相同,例如函式 A 可以定義 a 變數,函式 B 也可以定義 a 變數,兩個 a 變數是完全不同的變數。
引用自:全域變數、區域變數 - Python 教學 | STEAM 教育學習網
上面所說的名稱空間其實就是命名空間,各位可以稍微變通一下各種中文的叫法,不然就是直接使用英文會達成語言的一致性。
接下來讓我們繼續來看,一般有三種命名空間:
- 內建命名空間(built-in namespace),Python 中內建的名稱,例如函數名稱 abs、char 和異常名稱 BaseException、Exception 等等。
- 全域命名空間(global namespace),模組中定義的名稱,記錄了模組的變數,包括函數、類別、其它導入的模組、模組級的變數和常數。
- 局域命名空間(local namespace),函數中定義的名稱,記錄了函數的變數,包括函數的參數和局部定義的變數。(類別中定義的也是)

圖源:全域變數、區域變數 - Python 教學 | STEAM 教育學習網
當我們要使用變數,假設一變數名為 x,則 Python 要開始搜尋這個變數 x 時,會先從最內層開始尋找(local namespace),然後往外(global namespace -> built-in namespace)。
如果找不到變數 x,則會 NameError:
1 | NameError: name 'x' is not defined。 |
命名空間的生命週期:命名空間的生命週期取決於物件的作用域,如果物件執行完成,則該命名空間的生命週期就結束。 因此,我們無法從外部命名空間存取內部命名空間的物件。
什麼意思呢?請看以下範例:
1 | a = 0 |
輸出結果:
1 | The variable in the main program is: 0 |
注意,函式名稱也屬於變數名稱,如果將函式定義為 a,則會覆寫全域變數 a 的內容。
首先我們在一開始先宣告一個變數叫作 a,然後印出值。
之後我們定義一函數叫作 func(),裡面同樣宣告一樣的名稱叫做變數 a,然後也是印出它的值。
在函數裡面,我們可以再定義一個函數,叫作 inner_func() 做的跟 func() 一樣的事情。
然後進行呼叫,下一行印出 a 值,可以從輸出結果中看到,最一開始宣告的那個變數值並沒有被改變,這正是因為有了命名空間的關係。
不同的命名空間的變數並不會互相干擾,如下圖所示,不同的變數名稱可存在於不同的命名空間當中:

image source:Namespaces and Scope in Python - GeeksforGeeks
我們再看一個範例:
1 | a = 1 |
範例來自:全域變數、區域變數 - Python 教學 | STEAM 教育學習網
當我們在函數裡面沒有宣告變數 a,而函數內需要使用變數 a。遇到這情況就會像前面所說的,要使用變數時 Python 會自動尋找變數,尋找變數時會從內層開始搜尋,內層找不到的話就往外搜尋。
由於外層(global namespace)有一個變數叫作變數 a,於是函數就使用全域變數 a 來進行運算並列出。
而因為在 print 函數裡面進行運算的關係,所以這並不會改變全域變數的值,會改變的是在函數裡面的參數值。
剛剛我們提到一個概念,叫作生命週期,這個意思其實我們可以這樣理解:當一個變數作為函數的參數進入函數裡面進行運算,函數會將這個參數作為一個「短期內」變數,而當它完成運算這個動作要出來的時候,就會自動消失了。
也就是說進入函數的參數,它完成它的工作以後就自動「消失」了,這就有點像是自然界中有些昆蟲完成它的使命後(如繁衍下一代之後),就上天了一樣的意思。
不過這只是用函數來舉例子而已,遇到其他情況的話各位同學可以自主思考看看。
作用域(scope)
作用域就是一個 Python 程式可以直接存取命名空間的正文區域。
在一個 Python 程式中,直接存取一個變數,會從內到外依序存取所有的作用域直到找到,否則會出現未定義的錯誤。
Python 中,程式的變數並不是哪個位置都可以存取的,存取權限決定於這個變數是在哪裡賦值的。
變數的作用域決定了在哪一部分的程式,才可以存取哪個特定的變數名稱。
Python 的作用域共有 4 種,分別是:
- L(Local Scope):最內層,包含局域變數,例如一個函數/方法內部。
- E(Enclosing Scope):包含了非局域(non-local)也非全域(non-global)的變數。例如兩個巢狀函數,一個函數(或類別) A 裡面又包含了一個函數 B,那麼對於 B 中的名稱來說 A 中的作用域就為 nonlocal。
- G(Global Scope):目前腳本的最外層,例如目前模組的全域變數。
- B(Built-in Scope): 包含了內建的變數/關鍵字等,最後被搜尋。
尋找順序:L –> E –> G –> B。
在局域找不到,便會去局域外的局域找(例如:閉包),再找不到就會去全域找,或者去內建中找。

image source:閉包 ( Closure ) - Python 教學 | STEAM 教育學習網
簡單來說,作用域就是變數、常數、函式或其他定義語句可以被「存取到」的範圍,作用域的英文名「scope」即為範圍之意,內部的作用域無法影響到外部作用域。
閉包(closure)
閉包,從字面的意思翻譯就是一個「封閉的包裹」,在包裹外的人,無法拿到包裹裡的東西,如果你在包裹裡,就能盡情取用包裹內的東西,閉包可以保存在函式作用範圍內的狀態,不會受到其他函式的影響,且無法從其他函式取得閉包內的資料,也可避免建立許多全域變數互相干擾。
閉包定義如下:
- A 函式中定義了 B 函式。
- B 函式使用了 A 函式的變數。
- A 函式回傳了 B 函式。
閉包形式就是我們在說明全域變數跟局域變數之間的關係時範例所寫的:
1 | a = 0 |
在 func() 函數裡面又定義了一個函數叫做 inner_func(),而在 inner_func() 裡面使用的卻是外面 func() 的變數 a,因為在 inner_func() 找不到變數 a,所以才會往外找,但要注意的是此時 inner_func() 是沒有被呼叫的狀態下,也就是說他只是被定義而已,要被呼叫的話就要看下一行:return inner_func(),這時候才會執行裡面的內容。
在函數 A 裡面再定義一個函數 B,然後定義的函數 B 內容物會使用到函數 A 的變數,接下來函數 A 的內容繼續執行下去,直接回傳函數 B,執行函數 B 的內容,這就是閉包。
但是如果在 inner_func() 裡面定義函數的話又不一樣了:
1 | var = 0 # 全域作用域 |
這三個雖然名稱相同,但是是完全不一樣的變數,在 inner_func() 的變數是屬於 inner_func() 的,並不是直接使用 func() 本身的變數。
以下範例來自閉包 ( Closure ) - Python 教學 | STEAM 教育學習網:
下方的程式碼,會建立一個 avg 函式的閉包,執行後雖然 test() 執行了三次,但因為每次執行時保留下一個作用域的繫結關係,所以會不斷將傳入的數值進行計算,最後就會得到 11 的結果。
1 | def count(): # 建立一個 count 函式 |
註:「作用域的繫結關係」指在程式碼中,變數和函數的可見性和「生命週期」。在上面的程式碼中,avg 函數和 a 變數都在 count 函式的作用域內。當 count 函數被呼叫並回傳 avg 函式時,雖然 count 函數的執行已經結束,但由於閉包(Closure)的特性,avg 函式仍可以存取和修改 a 變數,這就是所謂的「作用域的繫結關係」。
不過上面的範例似乎有點小瑕疵,原本該網站所顯示的輸出結果會是:
1 | [10] |
但我們只要將 test(12) 的地方稍微修改一下,加上 print(test(12)),就會列印出 11.0。接下來讓我們重新整理一下範例的程式碼:
1 | def count(): # 建立一個 count 函式 |
在上面的程式碼中可以看到,閉包函數 avg 由於處於局域作用域,且在內部並沒有宣告任何的變數,而且要使用變數 a,這時候它就會自動尋找變數 a 的來源,因為在內部沒有看到宣告變數 a 的情形嘛,找不到就往外找,而在閉包函數外的函數(count())找到了變數 a,所以閉包函數 avg 就會使用那一個變數來進行相關的運算。
然後可以看到 count 函數最下面直接回傳 avg 函數,所以 test 實際上執行的會是 avg 函數的內容,而 test() 內的引數就會當作是 avg 函數的參數作使用,那當然接下來就會印出列表 a,以及回傳列表所有數值的平均囉。
global & nonlocal
當內部作用域想要修改外部作用域的變數時,我們就需要用到 global 或 nonlocal 關鍵字了,global 的範例如下所示:
1 | a = 0 # 全域變數 |
另外的特殊情形:
1 | a = 10 |
上面這段程式碼會直接出錯,因為局域變數並沒有被宣告,可以看到變數 a 被變數 a + 1 給賦值,但前面沒有宣告任何的變數 a,所以才導致出錯。
加上全域變數或是直接宣告一個局域變數 a,那麼這段程式碼即不會出錯:
1 | a = 10 |
上面的情況也可以使用參數傳遞,總之有多種方法可以使用,取決於你。
若我們想要在閉包內對閉包外函數之作用域進行修改變數的話,我們就必須使用到 nonlocal 這個關鍵字。
以下是一個範例(範例來自菜鳥教程):
1 | def outer(): |
這段程式碼中的 outer 函數內部,先是呼叫 inner() 函數,並且其內部對 num 進行 nonlocal(自由變數)宣告,之後將 num 賦值於 100,輸出 num 之值。
而 inner() 函數結束之後,跳回到 outer() 函數,繼續執行 print(num),這時候就會發現原本在 outer() 函數的 num 值被改變了。
接下來讓我們看看如果不寫 nonlocal 的話會怎樣呢?
1 | def outer(): |
欸,我們可以發現外部 outer() 函數的 num 值沒有被改變,雖然 outer()、inner() 兩者變數名稱一樣,但就像之前所說的,這是因為作用域的不同所導致兩者變數是不一樣的原因。
總結
lambda
lambda 是一種匿名函數(沒有名稱的函數),可以用來快速定義簡單的函數,而不需要用 def 關鍵字。
語法:
1 | lambda arguments: expression |
如 lambda x, y: x + y。
lambda 函數常用於一些函數,如 map()、filter()、sorted() 等函數中作為參數傳入。
裝飾器(Decorator)
裝飾器是一個接受函數並回傳另一個函數的高階函數,用來在不改變原有函數的前提下,為其增加額外功能。
用法是在函數定義上方加上 @裝飾器名稱。
@ 被稱為語法糖。
多個裝飾器
用多個裝飾器來裝飾同一個函數時,裝飾器將按照從下往上的順序依次裝飾。
多個參數處理
裝飾器能處理多個參數,可以透過可變位置參數(*args)、可變關鍵字參數(**kwargs)設定多個參數。
類別裝飾器
類別裝飾器是包含 __call__ 方法的類,它接受一個函數作為參數,並回傳一個新的函數。
命名空間與作用域
一般有三種命名空間:
- 內建命名空間(built-in namespace),Python 中內建的名稱,例如函數名稱 abs、char 和異常名稱 BaseException、Exception 等等。
- 全域命名空間(global namespace),模組中定義的名稱,記錄了模組的變數,包括函數、類別、其它導入的模組、模組級的變數和常數。
- 局域命名空間(local namespace),函數中定義的名稱,記錄了函數的變數,包括函數的參數和局部定義的變數。(類別中定義的也是)
當我們要使用變數,假設一變數名為 x,則 Python 要開始搜尋這個變數 x 時,會先從最內層開始尋找(local namespace),然後往外(global namespace -> built-in namespace)。
命名空間的生命週期:命名空間的生命週期取決於物件的作用域,如果物件執行完成,則該命名空間的生命週期就結束。因此,我們無法從外部命名空間存取內部命名空間的物件。
簡單來說:如果一個有一個參數傳遞至一函數中,它完成它的工作以後它就自動「退場」了,這就有點像是自然界中有些昆蟲完成它的使命後(如繁衍下一代之後),就上天了一樣的意思。
Python 的作用域共有 4 種,分別是:
- L(Local Scope):最內層,包含局域變數,例如一個函數/方法內部。
- E(Enclosing Scope):包含了非局域(non-local)也非全域(non-global)的變數。例如兩個巢狀函數,一個函數(或類別) A 裡面又包含了一個函數 B,那麼對於 B 中的名稱來說 A 中的作用域就為 nonlocal。
- G(Global Scope):目前腳本的最外層,例如目前模組的全域變數。
- B(Built-in Scope): 包含了內建的變數/關鍵字等,最後被搜尋。
尋找順序:L –> E –> G –> B。(同命名空間的順序,一樣從最內層開始往外找)
作用域簡單來說,就是變數、常數、函式或其他定義語句可以被「存取到」的範圍,作用域的英文名「scope」即為範圍之意,內部的作用域無法影響到外部作用域。
閉包定義如下:
- A 函式中定義了 B 函式。
- B 函式使用了 A 函式的變數。
- A 函式回傳了 B 函式。
基本上就是一個函數裡面還定義了一個函數,然後那一個函數能夠使用上一層函數的變數,而光有定義還不夠,必須要呼叫閉包函數才能執行裡面的內容(通常是直接用 return 那個閉包函數)。
global 關鍵字:宣告某全域變數名稱,在函數裡面能夠使用那個全域變數,也就是能夠使用函數外的變數。
nonlocal 關鍵字:宣告某自由變數名稱,在閉包函數內能夠使用上一層函數的變數,接下來同上解釋。
參考資料
閉包 ( Closure ) - Python 教學 | STEAM 教育學習網
Namespaces and Scope in Python - GeeksforGeeks
全域變數、區域變數 - Python 教學 | STEAM 教育學習網
裝飾器 decorator - Python 教學 | STEAM 教育學習網
匿名函式 lambda - Python 教學 | STEAM 教育學習網




