摘要:操作函數的函數叫做高階函數。這一節展示了高階函數可用作強大的抽象機制,極大提升語言的表現力。新的環境特性高階函數。這是因為局部函數的函數體的求值環境擴展于定義處的求值環境。這種命名慣例并不由解釋器強制,只是函數名稱的一部分。
1.6 高階函數
來源:1.6 Higher-Order Functions
譯者:飛龍
協議:CC BY-NC-SA 4.0
我們已經看到,函數實際上是描述復合操作的抽象,這些操作不依賴于它們的參數值。在square中,
>>> def square(x): return x * x
我們不會談論特定數值的平方,而是一個獲得任何數值平方的方法。當然,我們可以不定義這個函數來使用它,通過始終編寫這樣的表達式:
>>> 3 * 3 9 >>> 5 * 5 25
并且永遠不會顯式提及square。這種實踐適合類似square的簡單操作。但是對于更加復雜的操作會變得困難。通常,缺少函數定義會對我們非常不利,它會強迫我們始終工作在特定操作的層級上,這在語言中非常原始(這個例子中是乘法),而不是高級操作。我們應該從強大的編程語言索取的東西之一,是通過將名稱賦為常用模式來構建抽象的能力,以及之后直接使用抽象的能力。函數提供了這種能力。
我們將會在下個例子中看到,代碼中會反復出現一些常見的編程模式,但是使用一些不同函數來實現。這些模式也可以被抽象和給予名稱。
為了將特定的通用模式表達為具名概念,我們需要構造可以接受其他函數作為參數的函數,或者將函數作為返回值的函數。操作函數的函數叫做高階函數。這一節展示了高階函數可用作強大的抽象機制,極大提升語言的表現力。
1.6.1 作為參數的函數考慮下面三個函數,它們都計算總和。第一個,sum_naturals,計算截至n的自然數的和:
>>> def sum_naturals(n): total, k = 0, 1 while k <= n: total, k = total + k, k + 1 return total >>> sum_naturals(100) 5050
第二個,sum_cubes,計算截至n的自然數的立方和:
>>> def sum_cubes(n): total, k = 0, 1 while k <= n: total, k = total + pow(k, 3), k + 1 return total >>> sum_cubes(100) 25502500
第三個,計算這個級數中式子的和:
它會慢慢收斂于pi。
>>> def pi_sum(n): total, k = 0, 1 while k <= n: total, k = total + 8 / (k * (k + 2)), k + 4 return total >>> pi_sum(100) 3.121594652591009
這三個函數在背后都具有相同模式。它們大部分相同,只是名字、用于計算被加項的k的函數,以及提供k的下一個值的函數不同。我們可以通過向相同的模板中填充槽位來生成每個函數:
def(n): total, k = 0, 1 while k <= n: total, k = total + (k), (k) return total
這個通用模板的出現是一個強有力的證據,證明有一個實用抽象正在等著我們表現出來。這些函數的每一個都是式子的求和。作為程序的設計者,我們希望我們的語言足夠強大,便于我們編寫函數來自我表達求和的概念,而不僅僅是計算特定和的函數。我們可以在 Python 中使用上面展示的通用模板,并且把槽位變成形式參數來輕易完成它。
>>> def summation(n, term, next): total, k = 0, 1 while k <= n: total, k = total + term(k), next(k) return total
要注意summation接受上界n,以及函數term和next作為參數。我們可以像任何函數那樣使用summation,它簡潔地表達了求和。
>>> def cube(k): return pow(k, 3) >>> def successor(k): return k + 1 >>> def sum_cubes(n): return summation(n, cube, successor) >>> sum_cubes(3) 36
使用identity 函數來返回參數自己,我們就可以對整數求和:
>>> def identity(k): return k >>> def sum_naturals(n): return summation(n, identity, successor) >>> sum_naturals(10) 55
我們也可以逐步定義pi_sum,使用我們的summation抽象來組合組件。
>>> def pi_term(k): denominator = k * (k + 2) return 8 / denominator >>> def pi_next(k): return k + 4 >>> def pi_sum(n): return summation(n, pi_term, pi_next) >>> pi_sum(1e6) 3.14159065358989361.6.2 作為一般方法的函數
我們引入的用戶定義函數作為一種數值運算的抽象模式,便于使它們獨立于涉及到的特定數值。使用高階函數,我們開始尋找更強大的抽象類型:一些函數表達了計算的一般方法,獨立于它們調用的特定函數。
盡管函數的意義在概念上擴展了,我們對于如何求解調用表達式的環境模型也優雅地延伸到了高階函數,沒有任何改變。當一個用戶定義函數以一些實參調用時,形式參數會在最新的局部幀中綁定實參的值(它們可能是函數)。
考慮下面的例子,它實現了迭代改進的一般方法,并且可以用于計算黃金比例。迭代改進算法以一個方程的解的guess(推測值)開始。它重復調用update函數來改進這個推測值,并且調用test來檢查是否當前的guess“足夠接近”所認為的正確值。
>>> def iter_improve(update, test, guess=1): while not test(guess): guess = update(guess) return guess
test函數通常檢查兩個函數f和g在guess值上是否彼此接近。測試f(x)是否接近于g(x)也是計算的一般方法。
>>> def near(x, f, g): return approx_eq(f(x), g(x))
程序中測試相似性的一個常見方式是將數值差的絕對值與一個微小的公差值相比:
>>> def approx_eq(x, y, tolerance=1e-5): return abs(x - y) < tolerance
黃金比例,通常叫做phi,是經常出現在自然、藝術、和建筑中的數值。它可以通過iter_improve使用golden_update來計算,并且在它的后繼等于它的平方時收斂。
>>> def golden_update(guess): return 1/guess + 1 >>> def golden_test(guess): return near(guess, square, successor)
這里,我們已經向全局幀添加了多個綁定。函數值的描述為了簡短而有所刪節:
使用golden_update和golden_test參數來調用iter_improve會計算出黃金比例的近似值。
>>> iter_improve(golden_update, golden_test) 1.6180371352785146
通過跟蹤我們的求值過程的步驟,我們就可以觀察結果如何計算。首先,iter_improve的局部幀以update、test和guess構建。在iter_improve的函數體中,名稱test綁定到golden_test上,它在初始值guess上調用。之后,golden_test調用near,創建第三個局部幀,它將形式參數f和g綁定到square和successor上。
完成near的求值之后,我們看到golden_test為False,因為 1 并不非常接近于 2。所以,while子句代碼組內的求值過程,以及這個機制的過程會重復多次。
這個擴展后的例子展示了計算機科學中兩個相關的重要概念。首先,命名和函數允許我們抽象而遠離大量的復雜性。當每個函數定義不重要時,由求值過程觸發的計算過程是相當復雜的,并且我們甚至不能展示所有東西。其次,基于事實,我們擁有了非常通用的求值過程,小的組件組合在復雜的過程中。理解這個過程便于我們驗證和檢查我們創建的程序。
像通常一樣,我們的新的一般方法iter_improve需要測試來檢查正確性。黃金比例可以提供這樣一個測試,因為它也有一個閉式解,我們可以將它與迭代結果進行比較。
>>> phi = 1/2 + pow(5, 1/2)/2 >>> def near_test(): assert near(phi, square, successor), "phi * phi is not near phi + 1" >>> def iter_improve_test(): approx_phi = iter_improve(golden_update, golden_test) assert approx_eq(phi, approx_phi), "phi differs from its approximation"
新的環境特性:高階函數。
附加部分:我們在測試的證明中遺漏了一步。求出公差值e的范圍,使得如果tolerance為e的near(x, square, successor)值為真,那么使用相同公差值的approx_eq(phi, x)值為真。
1.6.3 定義函數 III:嵌套定義上面的例子演示了將函數作為參數傳遞的能力如何提高了編程語言的表現力。每個通用的概念或方程都能映射為自己的小型函數,這一方式的一個負面效果是全局幀會被小型函數弄亂。另一個問題是我們限制于特定函數的簽名:iter_improve 的update參數必須只接受一個參數。Python 中,嵌套函數的定義解決了這些問題,但是需要我們重新修改我們的模型。
讓我們考慮一個新問題:計算一個數的平方根。重復調用下面的更新操作會收斂于x的平方根:
>>> def average(x, y): return (x + y)/2 >>> def sqrt_update(guess, x): return average(guess, x/guess)
這個帶有兩個參數的更新函數和iter_improve不兼容,并且它只提供了一個介值。我們實際上只關心最后的平方根。這些問題的解決方案是把函數放到其他定義的函數體中。
>>> def square_root(x): def update(guess): return average(guess, x/guess) def test(guess): return approx_eq(square(guess), x) return iter_improve(update, test)
就像局部賦值,局部的def語句僅僅影響當前的局部幀。這些函數僅僅當square_root求值時在作用域內。和求值過程一致,局部的def語句在square_root調用之前并不會求值。
詞法作用域。局部定義的函數也可以訪問它們定義所在作用域的名稱綁定。這個例子中,update引用了名稱x,它是外層函數square_root的一個形參。這種在嵌套函數中共享名稱的規則叫做詞法作用域。嚴格來說,內部函數能夠訪問定義所在環境(而不是調用所在位置)的名稱。
我們需要兩個對我們環境的擴展來兼容詞法作用域。
每個用戶定義的函數都有一個關聯環境:它的定義所在的環境。
當一個用戶定義的函數調用時,它的局部幀擴展于函數所關聯的環境。
回到square_root,所有函數都在全局環境中定義,所以它們都關聯到全局環境,當我們求解square_root的前兩個子句時,我們創建了關聯到局部環境的函數。在
>>> square_root(256) 16.00000000000039
的調用中,環境首先添加了square_root的局部幀,并且求出def語句update和test(只展示了update):
隨后,update的名稱解析到這個新定義的函數上,它是向iter_improve傳入的參數。在iter_improve的函數體中,我們必須以初始值 1 調用update函數。最后的這個調用以一開始只含有g的局部幀創建了update的環境,但是之前的square_root幀上仍舊含有x的綁定。
這個求值過程中,最重要的部分是函數所關聯的環境變成了局部幀,它是函數求值的地方。這個改變在圖中以藍色箭頭高亮。
以這種方式,update的函數體能夠解析名稱x。所以我們意識到了詞法作用域的兩個關鍵優勢。
局部函數的名稱并不影響定義所在函數外部的名稱,因為局部函數的名稱綁定到了定義處的當前局部環境中,而不是全局環境。
局部函數可以訪問外層函數的環境。這是因為局部函數的函數體的求值環境擴展于定義處的求值環境。
update函數自帶了一些數據:也就是在定義處環境中的數據。因為它以這種方式封裝信息,局部定義的函數通常叫做閉包。
新的環境特性:局部函數定義。
1.6.4 作為返回值的函數我們的程序可以通過創建返回值是它們本身的函數,獲得更高的表現力。帶有詞法作用域的編程語言的一個重要特性就是,局部定義函數在它們返回時仍舊持有所關聯的環境。下面的例子展示了這一特性的作用。
在定義了許多簡單函數之后,composition是包含在我們的編程語言中的自然組合法。也就是說,提供兩個函數f(x)和g(x),我們可能希望定義h(x) = f(g(x))。我們可以使用現有工具來定義復合函數:
>>> def compose1(f, g): def h(x): return f(g(x)) return h >>> add_one_and_square = compose1(square, successor) >>> add_one_and_square(12) 169
compose1中的1表明復合函數和返回值都只接受一個參數。這種命名慣例并不由解釋器強制,1只是函數名稱的一部分。
這里,我們開始觀察我們在計算的復雜模型中投入的回報。我們的環境模型不需要任何修改就能支持以這種方式返回函數的能力。
1.6.5 Lambda 表達式目前為止,每次我們打算定義新的函數時,我們都會給它一個名稱。但是對于其它類型的表達式,我們不需要將一個間接產物關聯到名稱上。也就是說,我們可以計算a*b + c*d,而不需要給子表達式a*b或c*d,或者整個表達式來命名。Python 中,我們可以使用 Lambda 表達式憑空創建函數,它會求值為匿名函數。Lambda 表達式是函數體具有單個返回表達式的函數,不允許出現賦值和控制語句。
Lambda 表達式十分受限:它們僅僅可用于簡單的單行函數,求解和返回一個表達式。在它們適用的特殊情形中,Lambda 表達式具有強大的表現力。
>>> def compose1(f,g): return lambda x: f(g(x))
我們可以通過構造相應的英文語句來理解 Lambda 表達式:
lambda x : f(g(x)) "A function that takes x and returns f(g(x))"
一些程序員發現使用 Lambda 表達式作為匿名函數非常簡短和直接。但是,復合的 Lambda 表達式非常難以辨認,盡管它們很簡潔。下面的定義是是正確的,但是許多程序員不能很快地理解它:
>>> compose1 = lambda f,g: lambda x: f(g(x))
通常,Python 的代碼風格傾向于顯式的def語句而不是 Lambda 表達式,但是允許它們在簡單函數作為參數或返回值的情況下使用。
這種風格規范不是準則,你可以想怎么寫就怎么寫,但是,在你編寫程序時,要考慮某一天可能會閱讀你的程序的人們。如果你可以讓你的程序更易于理解,你就幫了人們一個忙。
Lambda 的術語是一個歷史的偶然結果,來源于手寫的數學符號和早期打字系統限制的不兼容。
使用 lambda 來引入過程或函數看起來是不正當的。這個符號要追溯到 Alonzo Church,他在 20 世紀 30 年代開始使用“帽子”符號;他把平方函數記為? . y × y。但是失敗的打字員將這個帽子移到了參數左邊,并且把它改成了大寫的 lambda:Λy . y × y;之后大寫的 lambda 就變成了小寫,現在我們就會在數學書里看到λy . y × y,以及在 Lisp 里看到(lambda (y) (* y y))。
-- Peter Norvig (norvig.com/lispy2.html)
盡管它的詞源不同尋常,Lambda 表達式和函數調用相應的形式語言,以及 Lambda 演算都成為了計算機科學概念的基礎,并在 Python 編程社區廣泛傳播。當我們學習解釋器的設計時,我們將會在第三章中重新碰到這個話題。
1.6.6 示例:牛頓法最后的擴展示例展示了函數值、局部定義和 Lambda 表達式如何一起工作來簡明地表達通常的概念。
牛頓法是一個傳統的迭代方法,用于尋找使數學函數返回值為零的參數。這些值叫做一元數學函數的根。尋找一個函數的根通常等價于求解一個相關的數學方程。
16 的平方根是滿足square(x) - 16 = 0的x值。
以 2 為底 32 的對數(例如 2 與某個指數的冪為 32)是滿足pow(2, x) - 32 = 0的x值。
所以,求根的通用方法會向我們提供算法來計算平方根和對數。而且,我們想要計算根的等式只包含簡單操作:乘法和乘方。
在我們繼續之前有個注解:我們知道如何計算平方根和對數,這個事實很容易當做自然的事情。并不只是 Python,你的手機和計算機,可能甚至你的手表都可以為你做這件事。但是,學習計算機科學的一部分是弄懂這些數如何計算,而且,這里展示的通用方法可以用于求解大量方程,而不僅僅是內建于 Python 的東西。
在開始理解牛頓法之前,我們可以開始編程了。這就是函數抽象的威力。我們簡單地將之前的語句翻譯成代碼:
>>> def square_root(a): return find_root(lambda x: square(x) - a) >>> def logarithm(a, base=2): return find_root(lambda x: pow(base, x) - a)
當然,在我們定義find_root之前,現在還不能調用任何函數,所以我們需要理解牛頓法如何工作。
牛頓法也是一個迭代改進算法:它會改進任何可導函數的根的推測值。要注意我們感興趣的兩個函數都是平滑的。對于
f(x) = square(x) - 16(細線)
f(x) = pow(2, x) - 32(粗線)
在二維平面上畫出x對f(x)的圖像,它展示了兩個函數都產生了光滑的曲線,它們在某個點穿過了 0。
由于它們是光滑的(可導的),這些曲線可以通過任何點上的直線來近似。牛頓法根據這些線性的近似值來尋找函數的根。
想象經過點(x, f(x))的一條直線,它與函數f(x)的曲線在這一點的斜率相同。這樣的直線叫做切線,它的斜率叫做f在x上的導數。
這條直線的斜率是函數值改變量與函數參數改變量的比值。所以,按照f(x)除以這個斜率來平移x,就會得到切線到達 0 時的x值。
我們的牛頓更新操作表達了跟隨這條切線到零的計算過程。我們通過在非常小的區間上計算函數斜率來近似得到函數的導數。
>>> def approx_derivative(f, x, delta=1e-5): df = f(x + delta) - f(x) return df/delta >>> def newton_update(f): def update(x): return x - f(x) / approx_derivative(f, x) return update
最后,我們可以定義基于newton_update(我們的迭代改進算法)的find_root函數,以及一個測試來觀察f(x)是否接近于 0。我們提供了一個較大的初始推測值來提升logarithm的性能。
>>> def find_root(f, initial_guess=10): def test(x): return approx_eq(f(x), 0) return iter_improve(newton_update(f), test, initial_guess) >>> square_root(16) 4.000000000026422 >>> logarithm(32, 2) 5.000000094858201
當你實驗牛頓法時,要注意它不總是收斂的。iter_improve的初始推測值必須足夠接近于根,而且函數必須滿足各種條件。雖然具有這些缺陷,牛頓法是一個用于解決微分方程的強大的通用計算方法。實際上,非常快速的對數算法和大整數除法也采用這個技巧的變體。
1.6.7 抽象和一等函數這一節的開始,我們以觀察用戶定義函數作為關鍵的抽象技巧,因為它們讓我們能夠將計算的通用方法表達為編程語言中的顯式元素。現在我們已經看到了高階函數如何讓我們操作這些通用方法來進一步創建抽象。
作為程序員,我們應該留意識別程序中低級抽象的機會,在它們之上構建,并泛化它們來創建更加強大的抽象。這并不是說,一個人應該總是盡可能以最抽象的方式來編程;專家級程序員知道如何選擇合適于他們任務的抽象級別。但是能夠基于這些抽象來思考,以便我們在新的上下文中能使用它們十分重要。高階函數的重要性是,它允許我們更加明顯地將這些抽象表達為編程語言中的元素,使它們能夠處理其它的計算元素。
通常,編程語言會限制操作計算元素的途徑。帶有最少限制的元素被稱為具有一等地位。一些一等元素的“權利和特權”是:
它們可以綁定到名稱。
它們可以作為參數向函數傳遞。
它們可以作為函數的返回值返回。
它們可以包含在好素具結構中。
Python 總是給予函數一等地位,所產生的表現力的收益是巨大的。另一方面,控制結構不能做到:你不能像使用sum那樣將if傳給一個函數。
1.6.8 函數裝飾器Python 提供了特殊的語法,將高階函數用作執行def語句的一部分,叫做裝飾器。
>>> def trace1(fn): def wrapped(x): print("-> ", fn, "(", x, ")") return fn(x) return wrapped >>> @trace1 def triple(x): return 3 * x >>> triple(12) ->( 12 ) 36
這個例子中,定義了高階函數trace1,它返回一個函數,這個函數在調用它的參數之前執行print語句來輸出參數。triple的def語句擁有一個注解,@trace1,它會影響def的執行規則。像通常一樣,函數triple被創建了,但是,triple的名稱并沒有綁定到這個函數上,而是綁定到了在新定義的函數triple上調用trace1的返回函數值上。在代碼中,這個裝飾器等價于:
>>> def triple(x): return 3 * x >>> triple = trace1(triple)
附加部分:實際規則是,裝飾器符號@可以放在表達式前面(@trace1僅僅是一個簡單的表達式,由單一名稱組成)。任何產生合適的值的表達式都可以。例如,使用合適的值,你可以定義裝飾器check_range,使用@check_range(1, 10)來裝飾函數定義,這會檢查函數的結果來確保它們是 1 到 10 的整數。調用check_range(1,10)會返回一個函數,之后它會用在新定義的函數上,在新定義的函數綁定到def語句中的名稱之前。感興趣的同學可以閱讀 Ariel Ortiz 編寫的一篇裝飾器的簡短教程來了解更多的例子。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/45494.html
摘要:為通用語言設計解釋器的想法可能令人畏懼。但是,典型的解釋器擁有簡潔的通用結構兩個可變的遞歸函數,第一個求解環境中的表達式,第二個在參數上調用函數。這一章接下來的兩節專注于遞歸函數和數據結構,它們是理解解釋器設計的基礎。 3.1 引言 來源:3.1 Introduction 譯者:飛龍 協議:CC BY-NC-SA 4.0 第一章和第二章描述了編程的兩個基本元素:數據和函數之間的...
摘要:對象表示信息,但是同時和它們所表示的抽象概念行為一致。通過綁定行為和信息,對象提供了可靠獨立的日期抽象。名稱來源于實數在中表示的方式浮點表示。另一方面,對象可以表示很大范圍內的分數,但是不能表示所有有理數。 2.1 引言 來源:2.1 Introduction 譯者:飛龍 協議:CC BY-NC-SA 4.0 在第一章中,我們專注于計算過程,以及程序設計中函數的作用。我們看到了...
摘要:序列不是特定的抽象數據類型,而是不同類型共有的一組行為。不像抽象數據類型,我們并沒有闡述如何構造序列。這兩個選擇器和一個構造器,以及一個常量共同實現了抽象數據類型的遞歸列表。 2.3 序列 來源:2.3 Sequences 譯者:飛龍 協議:CC BY-NC-SA 4.0 序列是數據值的順序容器。不像偶對只有兩個元素,序列可以擁有任意(但是有限)個有序元素。 序列在計算機科學中...
摘要:計算器語言解釋器的核心是叫做的遞歸函數,它會求解樹形表達式對象。到目前為止,我們在描述求值過程中所引用的表達式樹,還是概念上的實體。解析器實際上由兩個組件組成,詞法分析器和語法分析器。標記序列由叫做的詞法分析器產生,并被叫做語法分析器使用。 3.5 組合語言的解釋器 來源:3.5 Interpreters for Languages with Combination 譯者:飛龍 ...
摘要:實踐指南函數的藝術來源譯者飛龍協議函數是所有程序的要素,無論規模大小,并且在編程語言中作為我們表達計算過程的主要媒介。目前為止,我們討論了函數的形式特性,以及它們如何使用。第一行描述函數的任務。 1.4 實踐指南:函數的藝術 來源:1.4 Practical Guidance: The Art of the Function 譯者:飛龍 協議:CC BY-NC-SA 4.0 函...
閱讀 1049·2021-11-18 10:02
閱讀 1304·2021-09-23 11:22
閱讀 2607·2021-08-21 14:08
閱讀 1636·2019-08-30 15:55
閱讀 1720·2019-08-30 13:45
閱讀 3141·2019-08-29 16:52
閱讀 3092·2019-08-29 12:18
閱讀 1636·2019-08-26 13:36