內層函式——它們的優點是什麼?
讓我們看一下寫內層函式的三個常見原因。
記住:在Python中,函式是“一等公民”,這意味著它們和其他物件平起平坐(例如:整型,字串,列表,模組等)。你可以動態地建立和銷燬它們,把它們傳遞給其他函式,把它們作為值返回,等等。
本文使用Python 3.4.1版本
1.封裝
你使用內層函式來保護它們不受函式外部變化的影響,也就是說把它們從全域性作用域藏起來。
這裡有一個簡單的例子來強調這一概念:
1 2 3 4 5 6 7 8 |
def outer(num1): def inner_increment(num1): # hidden from outer code return num1 + 1 num2 = inner_increment(num1) print(num1, num2) inner_increment(10) # outer(10) |
嘗試呼叫inner_increment()
函式
1 2 3 4 |
Traceback (most recent call last): File "inner.py", line 7, in <module> inner_increment() NameError: name 'inner_increment' is not defined |
現在,把inner_increment
的呼叫註釋掉,再把對外部函式呼叫的註釋取消,outer(10)
,把10作為引數傳入:
1 |
10 11 |
請記住這僅僅是一個例子,儘管程式碼得到了期望的結果,但是使用一個前置的下劃線把
inner_increment()
函式變為“私有”函式:_inner_increment()
更好。
下面這個遞迴的例子與使用巢狀函式相比稍微好一些:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
def factorial(number): # error handling if not isinstance(number, int): raise TypeError("Sorry. 'number' must be an integer.") if not number >= 0: raise ValueError("Sorry. 'number' must be zero or positive.") def inner_factorial(number): if number <= 1: return 1 return number*inner_factorial(number-1) return inner_factorial(number) # call the outer function print(factorial(4)) |
同樣測試一下這段程式碼。使用這種設計模式的一個主要優勢在於:在外部函式中對全部引數執行了檢查,你可以在內部函式中跳過全部的檢查過程。
關於這個遞迴更加詳細的解釋請看Problem Solving with Algorithms and Data Structures
2.貫徹DRY(Don’t Repeat Yourself )原則
也許你有一個巨型函式,在很多很多地方執行一大段的程式碼。比如,你可能寫了一個函式用來處理檔案,並且你希望它既可以接受一個開啟檔案物件或是一個檔名:
1 2 3 4 5 6 7 8 9 |
def process(file_name): def do_stuff(file_process): for line in file_process: print(line) if isinstance(file_name, str): with open(file_name, 'r') as f: do_stuff(f) else: do_stuff(file_name) |
同樣,通常會把
do_stuff()
作為一個頂層私有函式,但是如果你想要把它作為一個內層函式隱藏起來,你也可以這樣做。
來個實際的例子怎麼樣?
讓我們假設你想知道紐約市全部WiFi熱點的數量。而且確實有提供這些資訊的原始資料: 資料。
訪問這個網站並下載CSV.
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 |
def process(file_name): def do_stuff(file_process): wifi_locations = {} for line in file_process: values = line.split(',') # Build the dict, and increment values wifi_locations[values[1]] = wifi_locations.get(values[1], 0) + 1 max_key = 0 for name, key in wifi_locations.items(): all_locations = sum(wifi_locations.values()) if key > max_key: max_key = key business = name print('There are {0} WiFi hot spots in NYC and {1} has the most with {2}.'.format( all_locations, business, max_key)) if isinstance(file_name, str): with open(file_name, 'r') as f: do_stuff(f) else: do_stuff(file_name) process("NAME_OF_THE.csv") |
執行函式:
1 |
There are 1251 WiFi hot spots in NYC and Starbucks has the most with 212. |
3.閉包和工廠函式
現在我們要談到使用內層函式最重要的原因了。目前為止我們看到的全部內層函式的例子都是普通函式,只不過它們湊巧巢狀在另外的函式之中了。換句話說,我們本可以用另外的方式定義這些函式(正如我們談到的那樣),並沒有什麼特別的理由去巢狀它們。
但是當我們談到閉包的時候,情況就不一樣了:你必須要利用巢狀函式。
什麼是閉包?
閉包無非是使內層函式在呼叫時記住它當前環境的狀態。初學者經常認為閉包就是內層函式,而且實際上它是由內層函式導致的。閉包在棧上“封閉”了區域性變數,使其在棧建立執行結束後仍然存在。
一個例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
def generate_power(number): """ Examples of use: >>raise_two = generate_power(2) >>raise_three = generate_power(3) >>print(raise_two(7)) 128 >>print(raise_three(5)) 243 """ # define the inner function ... def nth_power(power): return number ** power # ... which is returned by the factory function return nth_power |
此處發生了什麼?
- generate_power()函式是一個工廠方法——簡單說就是意味著它每次呼叫的時候會建立一個新函式,並返回這個新建立的函式,因此
raise_two
和raise_three
都是新建立的函式。 - 這個新的內層函式做了什麼呢?它接受一個單獨的引數,power,並且返回
number**power
。 - 內層函式從哪得到
number
的值呢?這就該閉包登場了:nth_power()
從外層函式得到power
的值,讓我們逐步檢視這一過程:- 呼叫外層函式:
generate_power(2)
- 生成一個
nth_power()
函式,它接受一個單一的引數power - 儲存
nth_power()
的狀態快照,其中number=2 - 把這個快照傳入
generate_power()
函式 - 返回
nth_power()
函式
- 呼叫外層函式:
換句話說,閉包函式“初始化”了nth_power()
函式並將其返回。現在無論你何時呼叫這個新返回的函式,它都會去檢視其私有的快照,也就是包含power=2
的那一個。
現實世界
來一個實際例子如何?
1 2 3 4 5 6 7 8 9 10 11 12 13 |
def has_permission(page): def inner(username): if username == 'Admin': return "'{0}' does have access to {1}.".format(username, page) else: return "'{0}' does NOT have access to {1}.".format(username, page) return inner current_user = has_permission('Admin Area') print(current_user('Admin')) random_user = has_permission('Admin Area') print(current_user('Not Admin')) |
這是一個簡化過的函式,用來檢查某個特定使用者是否有許可權訪問特定的頁面。你可以很容易的修改它從會話中抓取使用者資訊,檢視他們是否具有訪問當前路徑的正確憑證。我們可以查詢資料庫來檢視許可情況,然後根據是否具有正確的憑證來返回返回正確的檢視,而不是檢查user是否等於‘Admin’。
結論
閉包和工廠函式是內層函式最常見也最強大的用途。在大多數情況下,當你看到一個被裝飾的函式,裝飾器就是一個工程函式,它接受一個函式作為引數,然後返回一個新的函式,其閉包中包含了原函式。停一停。深呼吸。喝口咖啡。再讀一遍。
換句話說,裝飾器是一個語法糖,用來實現generate_power() 例子中提到的功能
我把這個例子留給你:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
def generate_power(exponent): def decorator(f): def inner(*args): result = f(*args) return exponent**result return inner return decorator @generate_power(2) def raise_two(n): return n print(raise_two(7)) @generate_power(3) def raise_three(n): return n print(raise_two(5)) |
如果你的程式碼編輯器執行,請並排對比著檢視generate_power(exponent)
和generate_power(number)
這兩個函式來搞清楚我們所討論的概念。(比如說Sublime Text有分欄瀏覽模式(譯註:shift+alt+2))
如何你還沒有編寫這兩個函式,開啟編輯器並開始編寫。對於新手程式設計師來講,編碼是一項親手實踐的活動,就像學習騎車,你必須去做才行——而且要獨立完成。所以請回到手上的任務來。當你輸入完成程式碼後,你可以清楚的看到它們產生了相同的結果,但是這之間是有不同的。對於那些從來沒用過裝飾器的人來說,注意到這些不同是理解它們的第一步——如果你要走這條路的話。
打賞支援我翻譯更多好文章,謝謝!
打賞譯者
打賞支援我翻譯更多好文章,謝謝!
任選一種支付方式