SICP Python 描述 1.3 定義新的函數(shù)

1.3 定義新的函數(shù)

來(lái)源:1.3 Defining New Functions

譯者:飛龍

協(xié)議:CC BY-NC-SA 4.0

我們已經(jīng)在 Python 中認(rèn)識(shí)了一些在任何強(qiáng)大的編程語(yǔ)言中都會(huì)出現(xiàn)的元素:

  1. 數(shù)值是內(nèi)建數(shù)據(jù),算數(shù)運(yùn)算是函數(shù)。
  2. 嵌套函數(shù)提供了組合操作的手段。
  3. 名稱到值的綁定提供了有限的抽象手段。

現(xiàn)在我們將要了解函數(shù)定義,一個(gè)更加強(qiáng)大的抽象技巧,名稱通過(guò)它可以綁定到復(fù)合操作上,并可以作為一個(gè)單元來(lái)引用。

我們通過(guò)如何表達(dá)“平方”這個(gè)概念來(lái)開(kāi)始。我們可能會(huì)說(shuō),“對(duì)一個(gè)數(shù)求平方就是將這個(gè)數(shù)乘上它自己”。在 Python 中就是:

>>> def square(x):
        return mul(x, x)

這定義了一個(gè)新的函數(shù),并賦予了名稱square。這個(gè)用戶定義的函數(shù)并不內(nèi)建于解釋器。它表示將一個(gè)數(shù)乘上自己的復(fù)合操作。定義中的x叫做形式參數(shù),它為被乘的東西提供一個(gè)名稱。這個(gè)定義創(chuàng)建了用戶定義的函數(shù),并且將它關(guān)聯(lián)到名稱square上。

函數(shù)定義包含def語(yǔ)句,它標(biāo)明了<name>(名稱)和一列帶有名字的<formal parameters>(形式參數(shù))。之后,return(返回)語(yǔ)句叫做函數(shù)體,指定了函數(shù)的<return expression>(返回表達(dá)式),它是函數(shù)無(wú)論什么時(shí)候調(diào)用都需要求值的表達(dá)式。

def <name>(<formal parameters>):
    return <return expression>

第二行必須縮進(jìn)!按照慣例我們應(yīng)該縮進(jìn)四個(gè)空格,而不是一個(gè)Tab,返回表達(dá)式并不是立即求值,它儲(chǔ)存為新定義函數(shù)的一部分,并且只在函數(shù)最終調(diào)用時(shí)會(huì)被求出。(很快我們就會(huì)看到縮進(jìn)區(qū)域可以跨越多行。)

定義了square之后,我們使用調(diào)用表達(dá)式來(lái)調(diào)用它:

>>> square(21)
441
>>> square(add(2, 5))
49
>>> square(square(3))
81

我們也可以在構(gòu)建其它函數(shù)時(shí),將square用作構(gòu)建塊。列入,我們可以輕易定義sum_squares函數(shù),它接受兩個(gè)數(shù)值作為參數(shù),并返回它們的平方和:

>>> def sum_squares(x, y):
        return add(square(x), square(y))
>>> sum_squares(3, 4)
25

用戶定義的函數(shù)和內(nèi)建函數(shù)以同種方法使用。確實(shí),我們不可能在sum_squares的定義中分辨出square是否構(gòu)建于解釋器中,從模塊導(dǎo)入還是由用戶定義。

1.3.1 環(huán)境

我們的 Python 子集已經(jīng)足夠復(fù)雜了,但程序的含義還不是非常明顯。如果形式參數(shù)和內(nèi)建函數(shù)具有相同名稱會(huì)如何呢??jī)蓚€(gè)函數(shù)是否能共享名稱而不會(huì)產(chǎn)生混亂呢?為了解決這些疑問(wèn),我們必須詳細(xì)描述環(huán)境。

表達(dá)式求值所在的環(huán)境由幀的序列組成,它們可以表述為一些盒子。每一幀都包含了一些綁定,它們將名稱和對(duì)應(yīng)的值關(guān)聯(lián)起來(lái)。全局幀只有一個(gè),它包含所有內(nèi)建函數(shù)的名稱綁定(只展示了absmax)。我們使用地球符號(hào)來(lái)表示全局。

賦值和導(dǎo)入語(yǔ)句會(huì)向當(dāng)前環(huán)境的第一個(gè)幀添加條目。到目前為止,我們的環(huán)境只包含全局幀。

>>> from math import pi
>>> tau = 2 * pi

def語(yǔ)句也將綁定綁定到由定義創(chuàng)建的函數(shù)上。定義square之后的環(huán)境如圖所示:

這些環(huán)境圖示展示了當(dāng)前環(huán)境中的綁定,以及它們所綁定的值(并不是任何幀的一部分)。要注意函數(shù)名稱是重復(fù)的,一個(gè)在幀中,另一個(gè)是函數(shù)的一部分。這一重復(fù)是有意的,許多不同的名字可能會(huì)引用相同函數(shù),但是函數(shù)本身只有一個(gè)內(nèi)在名稱。但是,在環(huán)境中由名稱檢索值只檢查名稱綁定。函數(shù)的內(nèi)在名稱不在名稱檢索中起作用。在我們之前看到的例子中:

>>> f = max
>>> f
<built-in function max>

名稱max是函數(shù)的內(nèi)在名稱,以及打印f時(shí)我們看到的名稱。此外,名稱maxf在全局環(huán)境中都綁定到了相同函數(shù)上。

在我們介紹 Python 的附加特性時(shí),我們需要擴(kuò)展這些圖示。每次我們這樣做的時(shí)候,我們都會(huì)列出圖示可以表達(dá)的新特性。

新的環(huán)境特性:賦值和用戶定義的函數(shù)定義。

1.3.2 調(diào)用用戶定義的函數(shù)

為了求出運(yùn)算符為用戶定義函數(shù)的調(diào)用表達(dá)式,Python 解釋器遵循與求出運(yùn)算符為內(nèi)建函數(shù)的表達(dá)式相似的過(guò)程。也就是說(shuō),解釋器求出操作數(shù)表達(dá)式,并且對(duì)產(chǎn)生的實(shí)參調(diào)用具名函數(shù)。

調(diào)用用戶定義的函數(shù)的行為引入了第二個(gè)局部幀,它只能由函數(shù)來(lái)訪問(wèn)。為了對(duì)一些實(shí)參調(diào)用用戶定義的函數(shù):

  1. 在新的局部幀中,將實(shí)參綁定到函數(shù)的形式參數(shù)上。
  2. 在當(dāng)前幀的開(kāi)頭以及全局幀的末尾求出函數(shù)體。

函數(shù)體求值所在的環(huán)境由兩個(gè)幀組成:第一個(gè)是局部幀,包含參數(shù)綁定,之后是全局幀,包含其它所有東西。每個(gè)函數(shù)示例都有自己的獨(dú)立局部幀。

這張圖包含兩個(gè)不同的 Python 解釋器層面:當(dāng)前的環(huán)境,以及表達(dá)式樹(shù)的一部分,它和要求值的代碼的當(dāng)前一行相關(guān)。我們描述了調(diào)用表達(dá)式的求值,用戶定義的函數(shù)(藍(lán)色)表示為兩部分的圓角矩形。點(diǎn)線箭頭表示哪個(gè)環(huán)境用于在每個(gè)部分求解表達(dá)式。

  • 上半部分展示了調(diào)用表達(dá)式的求值。這個(gè)調(diào)用表達(dá)式并不在任何函數(shù)里面,所以他在全局環(huán)境中求值。所以,任何里面的名稱(例如square)都會(huì)在全局幀中檢索。
  • 下半部分展示了square函數(shù)的函數(shù)體。它的返回表達(dá)式在上面的步驟1引入的新環(huán)境中求值,它將square的形式參數(shù)x的名稱綁定到實(shí)參的值-2上。

環(huán)境中幀的順序會(huì)影響由表達(dá)式中的名稱檢索返回的值。我們之前說(shuō)名稱求解為當(dāng)前環(huán)境中與這個(gè)名稱關(guān)聯(lián)的值。我們現(xiàn)在可以更精確一些:

  • 名稱求解為當(dāng)前環(huán)境中,最先發(fā)現(xiàn)該名稱的幀中,綁定到這個(gè)名稱的值。

我們關(guān)于環(huán)境、名稱和函數(shù)的概念框架建立了求值模型,雖然一些機(jī)制的細(xì)節(jié)仍舊沒(méi)有指明(例如綁定如何實(shí)現(xiàn)),我們的模型在描述解釋器如何求解調(diào)用表示上,變得更準(zhǔn)確和正確。在第三章我們會(huì)看到這一模型如何用作一個(gè)藍(lán)圖來(lái)實(shí)現(xiàn)編程語(yǔ)言的可工作的解釋器。

新的環(huán)境特性:函數(shù)調(diào)用。

1.3.3 示例:調(diào)用用戶定義的函數(shù)

讓我們?cè)僖淮慰紤]兩個(gè)簡(jiǎn)單的定義:

>>> from operator import add, mul
>>> def square(x):
        return mul(x, x)
>>> def sum_squares(x, y):
        return add(square(x), square(y))

以及求解下列調(diào)用表達(dá)式的過(guò)程:

>>> sum_squares(5, 12)
169

Python 首先會(huì)求出名稱sum_squares,它在全局幀綁定了用戶定義的函數(shù)。基本的數(shù)字表達(dá)式 5 和 12 求值為它們所表達(dá)的數(shù)值。

之后,Python 調(diào)用了sum_squares,它引入了局部幀,將x綁定為 5,將y綁定為 12。

這張圖中,局部幀指向它的后繼,全局幀。所有局部幀必須指向某個(gè)先導(dǎo),這些鏈接定義了當(dāng)前環(huán)境中的幀序列。

sum_square的函數(shù)體包含下列調(diào)用表達(dá)式:

   add     (  square(x)  ,  square(y)  )
 ________     _________     _________
"operator"   "operand 0"   "operand 1"

全部三個(gè)子表達(dá)式在當(dāng)前環(huán)境中求值,它開(kāi)始于標(biāo)記為sum_squares的幀。運(yùn)算符字表達(dá)式add是全局幀中發(fā)現(xiàn)的名稱,綁定到了內(nèi)建的加法函數(shù)上。兩個(gè)操作數(shù)子表達(dá)式必須在加法函數(shù)調(diào)用之前依次求值。兩個(gè)操作數(shù)都在當(dāng)前環(huán)境中求值,開(kāi)始于標(biāo)記為sum_squares的幀。在下面的環(huán)境圖示中,我們把這一幀叫做A,并且將指向這一幀的箭頭同時(shí)替換為標(biāo)簽A

在使用這個(gè)局部幀的情況下,函數(shù)體表達(dá)式mul(x, x)求值為 25。

我們的求值過(guò)程現(xiàn)在輪到了操作數(shù) 1,y的值為 12。Python 再次求出square的函數(shù)體。這次引入了另一個(gè)局部環(huán)境幀,將x綁定為 12。所以,操作數(shù) 1 求值為 144。

最后,對(duì)實(shí)參 25 和 144 調(diào)用加法會(huì)產(chǎn)生sum_squares函數(shù)體的最終值:169。

這張圖雖然復(fù)雜,但是用于展示我們目前為止發(fā)展出的許多基礎(chǔ)概念。名稱綁定到值上面,它延伸到許多局部幀中,局部幀在唯一的全局幀之上,全局幀包含共享名稱。表達(dá)式為樹(shù)形結(jié)構(gòu),以及每次子表達(dá)式包含用戶定義函數(shù)的調(diào)用時(shí),環(huán)境必須被擴(kuò)展。

所有這些機(jī)制的存在確保了名稱在表達(dá)式中正確的地方解析為正確的值。這個(gè)例子展示了為什么我們的模型需要所引入的復(fù)雜性。所有三個(gè)局部幀都包含名稱x的綁定。但是這個(gè)名稱在不同的幀中綁定到了不同的值上。局部幀分離了這些名稱。

1.3.4 局部名稱

函數(shù)實(shí)現(xiàn)的細(xì)節(jié)之一是實(shí)現(xiàn)者對(duì)形式參數(shù)名稱的選擇不應(yīng)影響函數(shù)行為。所以,下面的函數(shù)應(yīng)具有相同的行為:

>>> def square(x):
        return mul(x, x)
>>> def square(y):
        return mul(y, y)

這個(gè)原則 -- 也就是函數(shù)應(yīng)不依賴于編寫(xiě)者選擇的參數(shù)名稱 -- 對(duì)編程語(yǔ)言來(lái)說(shuō)具有重要的結(jié)果。最簡(jiǎn)單的結(jié)果就是函數(shù)參數(shù)名稱應(yīng)保留在函數(shù)體的局部范圍中。

如果參數(shù)不位于相應(yīng)函數(shù)的局部范圍中,square的參數(shù)x可能和sum_squares中的參數(shù)x產(chǎn)生混亂。嚴(yán)格來(lái)說(shuō),這并不是問(wèn)題所在:不同局部幀中的x的綁定是不相關(guān)的。我們的計(jì)算模型具有嚴(yán)謹(jǐn)?shù)脑O(shè)計(jì)來(lái)確保這種獨(dú)立性。

我們說(shuō)局部名稱的作用域被限制在定義它的用戶定義函數(shù)的函數(shù)體中。當(dāng)一個(gè)名稱不能再被訪問(wèn)時(shí),它就離開(kāi)了作用域。作用域的行為并不是我們模型的新事實(shí),它是環(huán)境的工作方式的結(jié)果。

1.3.5 實(shí)踐指南:選擇名稱

可修改的名稱并不代表形式參數(shù)的名稱完全不重要。反之,選擇良好的函數(shù)和參數(shù)名稱對(duì)于函數(shù)定義的人類(lèi)可解釋性是必要的。

下面的準(zhǔn)則派生于 Python 的代碼風(fēng)格指南,可被所有(非反叛)Python 程序員作為指南。一些共享的約定會(huì)使社區(qū)成員之間的溝通變得容易。遵循這些約定有一些副作用,我會(huì)發(fā)現(xiàn)你的代碼在內(nèi)部變得一致。

  1. 函數(shù)名稱應(yīng)該小寫(xiě),以下劃線分隔。提倡描述性的名稱。
  2. 函數(shù)名稱通常反映解釋器向參數(shù)應(yīng)用的操作(例如printaddsquare),或者結(jié)果(例如maxabssum)。
  3. 參數(shù)名稱應(yīng)小寫(xiě),以下劃線分隔。提倡單個(gè)詞的名稱。
  4. 參數(shù)名稱應(yīng)該反映參數(shù)在函數(shù)中的作用,并不僅僅是滿足的值的類(lèi)型。
  5. 當(dāng)作用非常明確時(shí),單個(gè)字母的參數(shù)名稱可以接受,但是永遠(yuǎn)不要使用l(小寫(xiě)的L)和O(大寫(xiě)的o),或者I(大寫(xiě)的i)來(lái)避免和數(shù)字混淆。

周期性對(duì)你編寫(xiě)的程序復(fù)查這些準(zhǔn)則,不用多久你的名稱會(huì)變得十分 Python 化。

1.3.6 作為抽象的函數(shù)

雖然sum_squares十分簡(jiǎn)單,但是它演示了用戶定義函數(shù)的最強(qiáng)大的特性。sum_squares函數(shù)使用square函數(shù)定義,但是僅僅依賴于square定義在輸入?yún)?shù)和輸出值之間的關(guān)系。

我們可以編寫(xiě)sum_squares,而不用考慮如何計(jì)算一個(gè)數(shù)值的平方。平方計(jì)算的細(xì)節(jié)被隱藏了,并可以在之后考慮。確實(shí),在sum_squares看來(lái),square并不是一個(gè)特定的函數(shù)體,而是某個(gè)函數(shù)的抽象,也就是所謂的函數(shù)式抽象。在這個(gè)層級(jí)的抽象中,任何能計(jì)算平方的函數(shù)都是等價(jià)的。

所以,僅僅考慮返回值的情況下,下面兩個(gè)計(jì)算平方的函數(shù)是難以區(qū)分的。每個(gè)都接受數(shù)值參數(shù)并且產(chǎn)生那個(gè)數(shù)的平方作為返回值。

>>> def square(x):
        return mul(x, x)
>>> def square(x):
        return mul(x, x-1) + x

換句話說(shuō),函數(shù)定義應(yīng)該能夠隱藏細(xì)節(jié)。函數(shù)的用戶可能不能自己編寫(xiě)函數(shù),但是可以從其它程序員那里獲得它作為“黑盒”。用戶不應(yīng)該需要知道如何實(shí)現(xiàn)來(lái)調(diào)用。Python 庫(kù)擁有這個(gè)特性。許多開(kāi)發(fā)者使用在這里定義的函數(shù),但是很少有人看過(guò)它們的實(shí)現(xiàn)。實(shí)際上,許多 Python 庫(kù)的實(shí)現(xiàn)并不完全用 Python 編寫(xiě),而是 C 語(yǔ)言。

1.3.7 運(yùn)算符

算術(shù)運(yùn)算符(例如+-)在我們的第一個(gè)例子中提供了組合手段。但是我們還需要為包含這些運(yùn)算符的表達(dá)式定義求值過(guò)程。

每個(gè)帶有中綴運(yùn)算符的 Python 表達(dá)式都有自己的求值過(guò)程,但是你通常可以認(rèn)為他們是調(diào)用表達(dá)式的快捷方式。當(dāng)你看到

>>> 2 + 3
5

的時(shí)候,可以簡(jiǎn)單認(rèn)為它是

>>> add(2, 3)
5

的快捷方式。

中綴記號(hào)可以嵌套,就像調(diào)用表達(dá)式那樣。Python 運(yùn)算符優(yōu)先級(jí)中采用了常規(guī)的數(shù)學(xué)規(guī)則,它指導(dǎo)了如何解釋帶有多種運(yùn)算符的復(fù)合表達(dá)式。

>>> 2 + 3 * 4 + 5
19

和下面的表達(dá)式的求值結(jié)果相同

>>> add(add(2, mul(3, 4)) , 5)
19

調(diào)用表達(dá)式的嵌套比運(yùn)算符版本更加明顯。Python 也允許括號(hào)括起來(lái)的子表達(dá)式,來(lái)覆蓋通常的優(yōu)先級(jí)規(guī)則,或者使表達(dá)式的嵌套結(jié)構(gòu)更加明顯:

>>> (2 + 3) * (4 + 5)
45

和下面的表達(dá)式的求值結(jié)果相同

>>> mul(add(2, 3), add(4, 5))
45

你應(yīng)該在你的程序中自由使用這些運(yùn)算符和括號(hào)。對(duì)于簡(jiǎn)單的算術(shù)運(yùn)算,Python 在慣例上傾向于運(yùn)算符而不是調(diào)用表達(dá)式。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,578評(píng)論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,701評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 178,691評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 63,974評(píng)論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,694評(píng)論 6 413
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 56,026評(píng)論 1 329
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,015評(píng)論 3 450
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 43,193評(píng)論 0 290
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,719評(píng)論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,442評(píng)論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,668評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,151評(píng)論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,846評(píng)論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 35,255評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 36,592評(píng)論 1 295
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,394評(píng)論 3 400
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,635評(píng)論 2 380

推薦閱讀更多精彩內(nèi)容