1.6 高階函數(shù)
我們已經(jīng)看到,函數(shù)是一種抽象方法,它用于描述獨(dú)立于它們的參數(shù)的復(fù)合運(yùn)算。比如函數(shù)square,
>>> def square(x):
return x * x
我們不是在談?wù)撎囟ㄖ档钠椒剑窃谟懻撘粋€(gè)能獲得任何數(shù)值平方數(shù)的方法。 當(dāng)然,我們可以不去定義這個(gè)函數(shù),總是編寫諸如以下的表達(dá)式:
>>> 3 * 3
9
>>> 5 * 5
25
并且永遠(yuǎn)不會(huì)明確地提到square。這種做法對(duì)于諸如square的簡(jiǎn)單計(jì)算就足夠了,但是對(duì)于更復(fù)雜的例子,例如abs或fib就將變得很困難。一般來說,缺乏函數(shù)定義將使我們處于不利之地,它迫使我們始終工作在非常原始的操作層面(這個(gè)例子中是乘法)而不是在較高級(jí)別操作。我們的程序能夠計(jì)算平方,但是我們的語言將缺乏表達(dá)平方概念的能力。
我們應(yīng)從強(qiáng)大的編程語言中索求的事情之一,就是通過將名稱分配給常用模式來構(gòu)建抽象的能力。函數(shù)提供這種能力。正如我們將在下面的例子中看到的,代碼中會(huì)反復(fù)出現(xiàn)一些常見的編程模式,但是使用一些不同函數(shù)來實(shí)現(xiàn)。這些模式也可以被抽象和給予名稱。
為了將特定的常用模式表達(dá)為具名概念,我們將需要構(gòu)造可以接受其他函數(shù)作為參數(shù)或?qū)⒑瘮?shù)作為返回值的函數(shù)。操縱函數(shù)的函數(shù)稱為高階函數(shù)。本節(jié)介紹了高階函數(shù)如何作為強(qiáng)大的抽象機(jī)制,極大增加了語言的表現(xiàn)力。
1.6.1 作為參數(shù)的函數(shù)
以下三個(gè)函數(shù)都能計(jì)算總和。第一個(gè)函數(shù)sum_naturals,計(jì)算 n個(gè)自然數(shù)的和:
>>> def sum_naturals(n):
total, k = 0, 1
while k <= n:
total, k = total + k, k + 1
return total
>>> sum_naturals(100)
5050
第二個(gè)函數(shù)sum_cubes,計(jì)算n個(gè)自然數(shù)的立方和:
>>> def sum_cubes(n):
total, k = 0, 1
while k <= n:
total, k = total + k*k*k, k + 1
return total
>>> sum_cubes(100)
25502500
第三個(gè)函數(shù)pi_sum,計(jì)算以下一系列項(xiàng)的總和:

它會(huì)慢慢收斂于
pi。
>>> def pi_sum(n):
total, k = 0, 1
while k <= n:
total, k = total + 8 / ((4*k-3) * (4*k-1)), k + 1
return total
>>> pi_sum(100)
3.1365926848388144
明顯地,這三個(gè)函數(shù)擁有一個(gè)相同的模式。 它們大部分是相同的,僅在函數(shù)名和用于計(jì)算被加項(xiàng)的k的方法有所不同。 我們可以通過填寫相同模板中的槽位來生成每個(gè)函數(shù):
def <name>(n):
total, k = 0, 1
while k <= n:
total, k = total + <term>(k), k + 1
return total
這種相同模板的存在是實(shí)用抽象出現(xiàn)的有力證據(jù)。 這些函數(shù)中的每一個(gè)都是求和術(shù)語。 作為程序的設(shè)計(jì)者,我們希望我們的語言足夠強(qiáng)大,以便我們可以編寫一個(gè)表達(dá)求和概念的函數(shù),而不是對(duì)特定的值進(jìn)行求和。在Python里,我們可以通過使用上面所示的通用模板,將“槽位”轉(zhuǎn)換為形式參數(shù):
在下面的例子中,summation求和有兩個(gè)參數(shù),上限n以及用來計(jì)算第k次值的函數(shù)term。 我們能像使用任何函數(shù)一樣使用summation,并簡(jiǎn)潔地表示結(jié)果。 您可以多花點(diǎn)時(shí)間來執(zhí)行這個(gè)例子,請(qǐng)注意到cube是如何綁定到局部名稱term上的并且如何確保1 * 1 * 1 + 2 * 2 * 2 + 3 * 3 * 3 = 36的計(jì)算正確的。 在此示例中,不再需要的幀會(huì)被刪除以節(jié)省空間。

使用`identity`函數(shù)來返回其參數(shù),我們還可以使用完全相同的`summation`求和函數(shù)來計(jì)算自然數(shù)之和。
>>> def summation(n, term):
total, k = 0, 1
while k <= n:
total, k = total + term(k), k + 1
return total
>>> def identity(x):
return x
>>> def sum_naturals(n):
return summation(n, identity)
>>> sum_naturals(10)
55
summation求和函數(shù)也可以直接調(diào)用,而不需要為特定序列定義另一個(gè)函數(shù)。
>>> summation(10, square)
385
我們可以使用我們的summation函數(shù)來抽象定義pi_sum函數(shù)。 我們傳遞參數(shù)1e6,這是1 * 10 ^ 6 = 1000000的簡(jiǎn)稱,結(jié)果會(huì)慢慢收斂于pi。
>>> def pi_term(x):
return 8 / ((4*x-3) * (4*x-1))
>>> def pi_sum(n):
return summation(n, pi_term)
>>> pi_sum(1e6)
3.141592153589902
1.6.2 作為一般方法的函數(shù)
我們引入用戶定義函數(shù)作為抽象機(jī)制,對(duì)數(shù)值運(yùn)算進(jìn)行抽象以使它們獨(dú)立于特定數(shù)值。對(duì)于高階函數(shù),我們開始尋找一種更強(qiáng)大的抽象類型:一些能表達(dá)一般計(jì)算方法的函數(shù),獨(dú)立于它們調(diào)用的特定函數(shù)。
函數(shù)的意義發(fā)生了擴(kuò)展,我們求解調(diào)用表達(dá)式的環(huán)境模型也可以適度地?cái)U(kuò)展到高階函數(shù)的情況。當(dāng)用戶定義函數(shù)調(diào)用參數(shù)時(shí),形式參數(shù)將會(huì)在最新的局部幀中綁定實(shí)參(可能是函數(shù))的值。
思考接下來的示例,其實(shí)現(xiàn)了迭代改進(jìn)的一般方法,并可以來計(jì)算golden ratio (黃金比例)。黃金比例,通常被稱為phi,是一個(gè)與1.6近似,經(jīng)常出現(xiàn)在自然、藝術(shù)、和建筑中的數(shù)值。
迭代改進(jìn)算法開始于對(duì)方程的解的guess(猜測(cè)值)。它重復(fù)調(diào)用update更新功能來改進(jìn)guess,并調(diào)用test來檢查當(dāng)前guess猜測(cè)值是否“足夠接近”預(yù)想的正確值。
>>> def improve(update, close, guess=1):
while not close(guess):
guess = update(guess)
return guess
improve改進(jìn)函數(shù)是重復(fù)細(xì)化的通用表達(dá)。 它不會(huì)具體指定要解決的問題:這些細(xì)節(jié)都留給update更新函數(shù)和close關(guān)閉函數(shù)來解決。
黃金比例的眾所周知的特性之一是可以通過反復(fù)疊加任何正數(shù)的倒數(shù)加上1來計(jì)算,而這個(gè)黃金比例屬性是一個(gè)小于它平方的數(shù)字。 我們可以將這些屬性表達(dá)為improve函數(shù)。
>>> def golden_update(guess):
return 1/guess + 1
>>> def square_close_to_successor(guess):
return approx_eq(guess * guess, guess + 1)
以上,我們調(diào)用了approx_eq:如果它的參數(shù)大致相等,則返回True。 我們可以將兩個(gè)數(shù)字差值的絕對(duì)值與一個(gè)很小的tolerance value(公差值)進(jìn)行比較。
>>> def approx_eq(x, y, tolerance=1e-15):
return abs(x - y) < tolerance
使用golden_update和square_close_to_successor作為參數(shù)來調(diào)用improve,將會(huì)計(jì)算出黃金比例的近似值。
>>> improve(golden_update, square_close_to_successor)
1.6180339887498951
通過跟蹤求值的步驟,我們可以看到結(jié)果是如何計(jì)算出來的。 首先,improve的局部環(huán)境被構(gòu)建起來,并為update 、test 和 guess 這三個(gè)參數(shù)綁定值。 在improve的函數(shù)體中,名字close被綁定到square_close_to_successor上,用于被初始值guess調(diào)用。 我們繼續(xù)跟蹤接下來的步驟來看看計(jì)算黃金比例的具體過程。

這個(gè)例子顯示了計(jì)算機(jī)科學(xué)中兩個(gè)相關(guān)的重要概念。 首先,命名和函數(shù)使我們能夠把復(fù)雜抽象為簡(jiǎn)單。 雖然每個(gè)函數(shù)定義都是不重要時(shí),但是由求值過程觸發(fā)的計(jì)算過程是相當(dāng)復(fù)雜的。 第二,我們擁有了非常通用的求值過程, 小組件能組合成復(fù)雜的程序。理解這個(gè)過程便于我們驗(yàn)證和檢查我們創(chuàng)建的程序。
一如以往,我們新的一般函數(shù)improve需要一個(gè)測(cè)試來檢查其正確性。 黃金比例可以提供這樣的測(cè)試,因?yàn)樗灿幸粋€(gè)精確的閉式解,我們可以將它與迭代結(jié)果進(jìn)行比較。
>>> from math import sqrt
>>> phi = 1/2 + sqrt(5)/2
>>> def improve_test():
approx_phi = improve(golden_update, square_close_to_successor)
assert approx_eq(phi, approx_phi), 'phi differs from its approximation'
>>> improve_test()
對(duì)于這個(gè)測(cè)試,沒有反饋消息就證明是好的:在執(zhí)行成功的assert語句之后,improve_test返回的是None。
1.6.3 定義函數(shù) III:嵌套定義
上面的例子演示了函數(shù)作為參數(shù)傳遞而顯著提高了編程語言的效率的能力。 每個(gè)通用概念或方程都能映射為小型函數(shù)。 這種方式的一個(gè)負(fù)面后果是,全局幀會(huì)變得混亂。 另一個(gè)問題是我們被特定函數(shù)的簽名所約束:improve的update參數(shù)必須只接受一個(gè)參數(shù)。 嵌套函數(shù)的定義解決了這兩個(gè)問題,但要求我們重新調(diào)整環(huán)境模型。
讓我們考慮一個(gè)新問題:計(jì)算一個(gè)數(shù)的平方根。 在編程語言中,“平方根”通??s寫為sqrt。 重復(fù)調(diào)用下面的更新操作會(huì)收斂于a的平方根:
>>> def average(x, y):
return (x + y)/2
>>> def sqrt_update(x, a):
return average(x, a/x)
這個(gè)帶有兩個(gè)參數(shù)的更新函數(shù)和improve不兼容(它需要接受兩個(gè)參數(shù),而不是一個(gè)參數(shù))。我們實(shí)際上只關(guān)心最后的平方根。這些問題的解決方案是把函數(shù)放到其他定義的函數(shù)體中。
>>> def sqrt(a):
def sqrt_update(x):
return average(x, a/x)
def sqrt_close(x):
return approx_eq(x * x, a)
return improve(sqrt_update, sqrt_close)
像局部賦值一樣,局部的def語句僅影響當(dāng)前的局部幀。這些函數(shù)僅僅當(dāng)sqrt求值時(shí)在作用域內(nèi)。和求值過程一致, 局部的def語句在sqrt調(diào)用之前并不會(huì)求值。
詞法作用域。局部定義的函數(shù)可以訪問定義作用域中的名稱綁定。在此示例中,sqrt_update引用名稱a,它是外層函數(shù)sqrt的形式參數(shù)。這種在嵌套函數(shù)中共享名稱的規(guī)則稱為詞法作用域。最重要的是,內(nèi)部函數(shù)可以訪問它們被定義的環(huán)境中的名稱(而不是它們被調(diào)用的位置)。
我們需要對(duì)我們的環(huán)境模型進(jìn)行兩個(gè)擴(kuò)展,以實(shí)現(xiàn)詞法作用域。
1.每個(gè)用戶定義的函數(shù)都有父環(huán)境:它的定義所在的環(huán)境。
2.當(dāng)用戶定義的函數(shù)被調(diào)用時(shí),其局部幀擴(kuò)展于其父環(huán)境。
在sqrt之前,所有函數(shù)都是在全局環(huán)境中定義的,所以它們都關(guān)聯(lián)到全局環(huán)境。相比之下,當(dāng)Python運(yùn)行sqrt的前兩個(gè)子句時(shí),它創(chuàng)建了與本地環(huán)境相關(guān)聯(lián)的函數(shù)。在
>>> sqrt(256)
16.0
的調(diào)用中,環(huán)境首先為sqrt添加一個(gè)局部幀,并為sqrt_update和sqrt_close的def語句求值。

函數(shù)值每個(gè)都有一個(gè)新的注釋,我們將在現(xiàn)在的環(huán)境圖中包含一個(gè)父級(jí)。 函數(shù)值的父項(xiàng)是定義該函數(shù)的環(huán)境的第一個(gè)框架。 沒有父注釋的功能是在全球環(huán)境中定義的。 當(dāng)調(diào)用用戶定義的函數(shù)時(shí),創(chuàng)建的框具有與該功能相同的父級(jí)。
隨后,sqrt_update的名稱解析到新定義的函數(shù)上,該函數(shù)是向improve傳入的參數(shù)。 在improve的函數(shù)體中,我們必須以初始值 1 調(diào)用update函數(shù)(綁定到sqrt_update)。最后這個(gè)調(diào)用創(chuàng)建一個(gè)sqrt_update的環(huán)境,該環(huán)境一開始僅包含x的局部幀,但是之前的sqrt幀仍然包含一個(gè)a的綁定。

此求值過程最關(guān)鍵的部分是將
sqrt_update的父級(jí)傳輸?shù)酵ㄟ^調(diào)用sqrt_update創(chuàng)建的幀。這個(gè)幀也用[parent = f1]注釋。
擴(kuò)展環(huán)境。環(huán)境可以由任意多的幀組成,總是以全局框架結(jié)束。在此sqrt示例之前,環(huán)境最多有兩幀:局部幀和全局幀。通過調(diào)用在其他函數(shù)中定義的函數(shù),通過嵌套的def語句,我們可以創(chuàng)建更多的幀。對(duì)sqrt_update調(diào)用的環(huán)境由三個(gè)幀組成:局部幀sqrt_update,定義sqrt_update的sqrt幀(標(biāo)記為f1)和全局幀。
sqrt_update函數(shù)體中的返回表達(dá)式可以通過跟隨這一系列幀來解析值。我們可以通過查找在當(dāng)前環(huán)境的名稱找到綁定到該名稱的第一個(gè)值。 Python首先在sqrt_update幀中檢查 -- 不存在。接下來,Python檢查父幀中f1,并找到a綁定到256。
因此,我們意識(shí)到了Python中詞法作用域的兩個(gè)主要優(yōu)點(diǎn)。
- 局部函數(shù)的名稱不會(huì)影響到定義函數(shù)外部的名稱,因?yàn)榫植亢瘮?shù)名稱將綁定到了定義處的當(dāng)前局部環(huán)境中,而不是全局環(huán)境中。
- 局部函數(shù)可以訪問外層函數(shù)的環(huán)境,這是因?yàn)榫植亢瘮?shù)的函數(shù)體的求值環(huán)境擴(kuò)展于定義處的求值環(huán)境。
sqrt_update函數(shù)自帶有一些數(shù)據(jù):在定義它的環(huán)境中引用的值。因?yàn)樗赃@種方式“封裝”信息,所以局部定義的函數(shù)通常被稱為閉包。
1.6.4 作為返回值的函數(shù)
我們可以通過創(chuàng)建返回值本身是函數(shù)的函數(shù)提高效率。 帶有詞法作用域的編程語言的一個(gè)重要特性就是,局部定義函數(shù)在它們返回時(shí)仍舊持有所關(guān)聯(lián)的環(huán)境。 以下示例展示了這一特性。
在定義了許多簡(jiǎn)單函數(shù)之后,composition函數(shù)就是包含在我們的編程語言中的自然組合方法。 也就是說,給定兩個(gè)函數(shù)f(x)和g(x),我們可能想要定義h(x)= f(g(x))。 我們可以使用我們現(xiàn)有的工具來定義復(fù)合函數(shù):
>>> def compose1(f, g):
def h(x):
return f(g(x))
return h
此示例的環(huán)境圖顯示了f和g是如何正確解析的,即使它們存在沖突的名稱。

compose1中的1意味著復(fù)合函數(shù)只采用單個(gè)參數(shù)。 該命名慣例不由解釋器強(qiáng)制執(zhí)行; 1只是函數(shù)名稱的一部分。
在這里,我們開始觀察我們努力在計(jì)算的復(fù)雜模型中投入的回報(bào)。我們的環(huán)境模型不需要任何修改就能支持以這種方式返回函數(shù)的能力。
1.6.5 例子:牛頓法
這個(gè)擴(kuò)展示例顯示了函數(shù)返回值和局部定義如何一起工作來簡(jiǎn)明扼要地表達(dá)通常概念。我們將實(shí)現(xiàn)一種能廣泛應(yīng)用于機(jī)器學(xué)習(xí),科學(xué)計(jì)算,硬件設(shè)計(jì)和優(yōu)化的算法。
牛頓法是一種經(jīng)典的迭代方法,用于查找使數(shù)學(xué)函數(shù)返回值為零的參數(shù)。這些值稱為函數(shù)的根。找到一個(gè)函數(shù)的根通常等價(jià)于解決一些有意思的問題,例如計(jì)算平方根。
開始前的激勵(lì):您當(dāng)然知道該如何計(jì)算平方根。不只是Python,您的手機(jī),網(wǎng)絡(luò)瀏覽器或計(jì)算器都可以為您做到這一點(diǎn)。然而,學(xué)習(xí)計(jì)算機(jī)科學(xué)的一部分是弄懂這些數(shù)如何計(jì)算,而且,這里展示的通用方法可以用于求解 大量方程,而不僅僅是內(nèi)建于 Python 的東西。
牛頓的方法是一種迭代改進(jìn)算法:它可以改進(jìn)任何可導(dǎo)函數(shù)的根的推測(cè)值,這意味著它可以在任何點(diǎn)被直線逼近。牛頓的方法遵循這些線性近似來找到函數(shù)根。
想象經(jīng)過點(diǎn) (x, f(x)) 的一條直線,它與函數(shù) f(x) 的曲線在這一點(diǎn)的斜率相同。這樣的直線叫做tangent切線,它的斜率叫做 f 在 x 上的derivative導(dǎo)數(shù)。
這條直線的斜率是函數(shù)值改變量與函數(shù)參數(shù)改變量的比值。所以,按照 f(x) 除以這個(gè)斜率來 平移 x ,就會(huì)得到切線到達(dá) 0 時(shí)的 x 值。

函數(shù)
newton_update表達(dá)了跟隨這條切線到零的計(jì)算過程。
>>> def newton_update(f, df):
def update(x):
return x - f(x) / df(x)
return update
最后,我們可以定義基于newton_update(我們的迭代改進(jìn)算法)的find_root函數(shù)和比較測(cè)試以查看f(x)是否接近0。
>>> def find_zero(f, df):
def near_zero(x):
return approx_eq(f(x), 0)
return improve(newton_update(f, df), near_zero)
計(jì)算根.使用牛頓法,我們可以計(jì)算任意度n的根。 a的n次方根為x,使得x·x·x ... x = a,其中x重復(fù)n次。 例如,
- 64的平方根為8,因?yàn)?·8 = 64。
- 64的立方根為4,因?yàn)?·4·4 = 64。
- 64的六次方根是2,因?yàn)??2?2?2?2?2= 64。
通過繪制n等于2, 3和6以及a等于64的曲線,我們可以看出以下關(guān)系。

我們首先通過定義
f及其導(dǎo)數(shù)df來實(shí)現(xiàn)square_root。 我們從微積分中知道f(x) = x^2 - a的導(dǎo)數(shù)是線性函數(shù)df(x) = 2*x。
>>> def square_root_newton(a):
def f(x):
return x * x- a
def df(x):
return 2 * x
return find_zero(f, df)
>>> square_root_newton(64)
8.0
以下是n次方根的情況:
>>> def power(x, n):
"""Return x * x * x * ... * x for x repeated n times."""
product, k = 1, 0
while k < n:
product, k = product * x, k + 1
return product
>>> def nth_root_of_a(n, a):
def f(x):
return power(x, n) - a
def df(x):
return n * power(x, n-1)
return find_zero(f, df)
>>> nth_root_of_a(2, 64)
8.0
>>> nth_root_of_a(3, 64)
4.0
>>> nth_root_of_a(6, 64)
2.0
所有這些計(jì)算中的近似誤差可以通過減少approx_eq的tolerance而降低。
當(dāng)您嘗試使用牛頓法時(shí),請(qǐng)注意它不總是收斂的。improve的初始猜測(cè)值必須足夠接近根,并且必須滿足函數(shù)的各種條件。 盡管有這個(gè)缺點(diǎn),牛頓法是一個(gè)能解決微分方程的強(qiáng)大的通用計(jì)算方法。 在現(xiàn)代計(jì)算機(jī)中,非常快速的對(duì)數(shù)算法和大整數(shù)除法也使用了該技巧的變體。
1.6.6 函數(shù)柯里化
我們可以使用高階函數(shù)將一個(gè)接受多參數(shù)的函數(shù)轉(zhuǎn)換成接受一個(gè)單一參數(shù)(最初函數(shù)的第一個(gè)參數(shù))的函數(shù)。 具體地說,給定函數(shù)f(x,y),我們可以定義函數(shù)g,使得g(x)(y)等價(jià)于f(x,y)。 這里,g是一個(gè)高階函數(shù),它接受單個(gè)參數(shù)x,并返回另一個(gè)接受單個(gè)參數(shù)y的函數(shù)。 這種轉(zhuǎn)變叫做currying函數(shù)柯里化。
例如,我們可以定義一個(gè)pow函數(shù)的柯里化版本:
>>> def curried_pow(x):
def h(y):
return pow(x, y)
return h
>>> curried_pow(2)(3)
8
一些編程語言(如Haskell)只允許函數(shù)接受單個(gè)參數(shù)的,因此程序員必須對(duì)所有多參數(shù)程序進(jìn)行currying柯里化。 在更通用的語言(如Python)中,當(dāng)我們需要只接受一個(gè)參數(shù)的函數(shù)時(shí),currying很有用。 例如,map模式將單參數(shù)函數(shù)應(yīng)用于一系列值。 在隨后的章節(jié)中,我們將看到更多的map模式的例子。至于現(xiàn)在,我們可以在一個(gè)函數(shù)中實(shí)現(xiàn)這個(gè)模式:
>>> def map_to_range(start, end, f):
while start < end:
print(f(start))
start = start + 1
我們可以使用map_to_range和curried_pow來計(jì)算2的前10個(gè)冪,而不是專門寫一個(gè)函數(shù):
>>> map_to_range(0, 10, curried_pow(2))
1
2
4
8
16
32
64
128
256
512
我們可以類似地使用相同的兩個(gè)函數(shù)來計(jì)算其他數(shù)字的冪函數(shù)。 currying可以讓我們做到,它不需要為我們希望計(jì)算的冪的每一個(gè)數(shù)寫一個(gè)特定的函數(shù)。
在上面的例子中,我們手動(dòng)對(duì)pow函數(shù)進(jìn)行currying轉(zhuǎn)換,得到curried_pow。 相反,我們可以定義自動(dòng)化currying的函數(shù),以及反向currying的轉(zhuǎn)換:
>>> def curry2(f):
"""Return a curried version of the given two-argument function."""
def g(x):
def h(y):
return f(x, y)
return h
return g
>>> def uncurry2(g):
"""Return a two-argument version of the given curried function."""
def f(x, y):
return g(x)(y)
return f
>>> pow_curried = curry2(pow)
>>> pow_curried(2)(5)
32
>>> map_to_range(0, 10, pow_curried(2))
1
2
4
8
16
32
64
128
256
512
curry2函數(shù)使用了雙參數(shù)函數(shù)f,并返回單參數(shù)函數(shù)g。 當(dāng)g接受參數(shù)x時(shí),它返回一個(gè)單參數(shù)函數(shù)h。 當(dāng)h接受參數(shù)y時(shí),它調(diào)用了f(x,y)。 因此,curry2(f)(x)(y)等價(jià)于f(x,y)。 uncurry2函數(shù)反轉(zhuǎn)currying變換,使得uncurry2(curry2(f))等效于f。
>>> uncurry2(pow_curried)(2, 5)
32
1.6.7 Lambda 表達(dá)式
到目前為止,每當(dāng)我們想要定義新的函數(shù)時(shí),我們都需要給它一個(gè)名字。 但是對(duì)于其他類型的表達(dá)式,我們不需要將中間值關(guān)聯(lián)到名稱上。 也就是說,我們可以計(jì)算a * b + c * d,而不用命名子表達(dá)式a * b或c * d。 在Python中,我們可以使用lambda表達(dá)式創(chuàng)建函數(shù),該表達(dá)式會(huì)求值為匿名函數(shù)。 lambda表達(dá)式是函數(shù)體具有單個(gè)返回表達(dá)式的函數(shù),不允許出現(xiàn)賦值和控制語句。
>>> def compose1(f, g):
return lambda x: f(g(x))
我們可以通過構(gòu)建相應(yīng)的英文句子來理解lambda表達(dá)式的結(jié)構(gòu):

lambda表達(dá)式的結(jié)果稱為lambda函數(shù)。 它沒有內(nèi)在名稱(因此Python為該名稱打印<lambda>),但它的行為就像任何其他函數(shù)一樣。
>>> s = lambda x: x * x
>>> s
<function <lambda> at 0xf3f490>
>>> s(12)
144
在環(huán)境圖示中,lambda表達(dá)式的結(jié)果也是用希臘字母λ(lambda)命名的函數(shù)。 我們的示例可以用lambda表達(dá)式表達(dá)得相當(dāng)簡(jiǎn)潔緊湊。

一些程序員發(fā)現(xiàn)使用lambda表達(dá)式的匿名函數(shù)更簡(jiǎn)短直接。 但是復(fù)合的lambda表達(dá)式是非常難以辨認(rèn)的,盡管它們很簡(jiǎn)潔。 以下定義是正確的,但很多程序員很難理解它。
>>> compose1 = lambda f,g: lambda x: f(g(x))
一般來說,Python代碼風(fēng)格傾向于顯式的def語句而不是 Lambda 表達(dá)式,但是允許它們?cè)诤?jiǎn)單函數(shù)作為參數(shù)或返回值的情況下使用。
這樣的風(fēng)格規(guī)則只是指導(dǎo); 你可以按你想要的方式進(jìn)行編程。 但是,在你編寫程序時(shí),要考慮某一天可能會(huì)閱讀你的程序的人們。如果你可以讓你的程序更易于理解,你就幫了人們一個(gè)忙。
術(shù)語lambda是一個(gè)歷史的偶然結(jié)果, 來源于手寫的數(shù)學(xué)符號(hào)和早期打字系統(tǒng)限制的不兼容。
It may seem perverse to use lambda to introduce a procedure/function. The notation goes back to Alonzo Church, who in the 1930's started with a "hat" symbol; he wrote the square function as "? . y × y". But frustrated typographers moved the hat to the left of the parameter and changed it to a capital lambda: "Λy . y × y"; from there the capital lambda was changed to lowercase, and now we see "λy . y × y" in math books and (lambda (y) (* y y)) in Lisp.
—Peter Norvig (norvig.com/lispy2.html)
盡管它們具有不尋常的詞源,lambda表達(dá)式和函數(shù)調(diào)用相應(yīng)形式的語言,以及l(fā)ambda演算都成為了計(jì)算機(jī)科學(xué)概念的基礎(chǔ),并在 Python編程社區(qū)廣泛傳播。 當(dāng)我們?cè)诘?章研究解釋器的設(shè)計(jì)時(shí),我們將重新討論這個(gè)話題。
1.6.8 抽象和一等函數(shù)
用戶定義函數(shù)是一個(gè)關(guān)鍵的抽象機(jī)制,因?yàn)樗鼈冊(cè)试S我們將計(jì)算的通用方法表達(dá)為編程語言中的顯式元素。現(xiàn)在我們已經(jīng)看到了高階函數(shù)如何讓我們操縱這些通用方法來創(chuàng)建進(jìn)一步的抽象。
作為程序員,我們應(yīng)該留意程序中的低級(jí)抽象,在它們之上構(gòu)架并將其概括為更強(qiáng)大的抽象。這不是說應(yīng)該總以最抽象的方式編寫程序; 專家程序員知道如何選擇適合他們?nèi)蝿?wù)的抽象級(jí)別。但是,重要的是能夠從這些抽象的角度思考,以便我們可以在新的環(huán)境中應(yīng)用它們。高階函數(shù)的意義,在于它們使我們能夠?qū)⑦@些抽象明確地表示為編程元素,以便像處理其他計(jì)算元素那樣處理它們。
通常,編程語言會(huì)對(duì)計(jì)算元素被操縱的方式施加限制。具有最少限制的要素具有一等的地位。一等元素的一些“權(quán)利和特權(quán)”是:
- 它們可以被綁定到名字。
- 它們可以作為參數(shù)傳遞給函數(shù)。
- 它們可以作為函數(shù)的返回值返回。
- 它們可以被包括在數(shù)據(jù)結(jié)構(gòu)中。
Python 總是給予函數(shù)一等地位, 所產(chǎn)生的表現(xiàn)力的收益是巨大的。
1.6.9 函數(shù)裝飾器
Python提供了特殊的語法, 將高階函數(shù)用作執(zhí)行def語句的一部分,叫做裝飾器。 也許最常見的例子是trace跟蹤。
>>> def trace(fn):
def wrapped(x):
print('-> ', fn, '(', x, ')')
return fn(x)
return wrapped
>>> @trace
def triple(x):
return 3 * x
>>> triple(12)
-> <function triple at 0x102a39848> ( 12 )
36
在這個(gè)例子中,定義了一個(gè)高階函數(shù)trace跟蹤,它返回一個(gè)函數(shù),該函數(shù)在調(diào)用它的參數(shù)之前執(zhí)行print語句來輸出參數(shù)。triple的def語句有一個(gè)注釋,@trace,它影響了def的執(zhí)行規(guī)則。 像往常一樣,函數(shù)triple被創(chuàng)建。 但是,triple名稱沒有綁定在此函數(shù)上。 相反,triple名稱綁定到新定義的triple上的調(diào)用trace的返回函數(shù)值上。 在代碼中,這個(gè)裝飾器相當(dāng)于:
>>> def triple(x):
return 3 * x
>>> triple = trace(triple)
在與此文本相關(guān)的項(xiàng)目中,裝飾器用于跟蹤,以及選擇在命令行運(yùn)行程序時(shí)要調(diào)用哪些函數(shù)。
附加部分。 裝飾器符號(hào)@后可以跟隨一個(gè)調(diào)用表達(dá)式。 首先求解@之后的表達(dá)式,然后是def語句,最后求解出的裝飾器表達(dá)式的結(jié)果被應(yīng)用于新定義的函數(shù),結(jié)果綁定到def聲明的名稱上。 如果您感興趣的話,可以閱讀Ariel Ortiz編寫的裝飾器簡(jiǎn)短教程。
下一節(jié):SICP 第一章 使用函數(shù)抽象概念 1.7 遞歸函數(shù)