我們已經(jīng)在Python中確定了一些在任何強(qiáng)大的編程語言中都有的元素:
1.數(shù)字和算術(shù)運(yùn)算是基本的內(nèi)置數(shù)據(jù)值和函數(shù)。
2.嵌套函數(shù)提供了組合操作的方法。
3.將名稱綁定到值的方式提供了有限的抽象方法。
現(xiàn)在我們將了解函數(shù)定義,一個更強(qiáng)大的抽象技術(shù),通過該技術(shù)可以將名稱綁定到復(fù)合操作上,然后將其作為單元引用。
我們首先研究如何表達(dá)square
平方的這個概念。 我們可能會說:“對數(shù)求平方就是將數(shù)自己乘上自己?!痹赑ython中的表達(dá)如下:
>>> def square(x):
return mul(x, x)
它定義了一個賦予了名稱square
的新函數(shù)。 這個用戶定義的函數(shù)并沒有內(nèi)置到解釋器中。 它代表著自己和自己相乘的復(fù)合操作。 這個定義中的x
稱為形式參數(shù),它為被乘的東西提供一個名稱。 該定義創(chuàng)建了此用戶定義的函數(shù),并將其與名稱square
相關(guān)聯(lián)。
如何定義一個函數(shù)。 函數(shù)定義包含一個def
語句,該語句包含了<name>
和一個帶有名字的<formal parameter>
(形式參數(shù)),然后是一個稱為函數(shù)體的return
返回語句,該語句指定了函數(shù)的<return expression>(返回表達(dá)式),這是一個每次函數(shù)調(diào)用都需要求值的表達(dá)式:
def <name>(<formal parameters>):
return <return expression>
第二行必須縮進(jìn) - 按照慣例大多數(shù)程序員使用四個空格來縮進(jìn)。 返回表達(dá)式不會立即求值; 它被存儲為新定義函數(shù)的一部分,并且僅在函數(shù)最終被調(diào)用時被求解。
定義了square
后,我們可以用表達(dá)式來調(diào)用它:
>>> square(21)
441
>>> square(add(2, 5))
49
>>> square(square(3))
81
我們還可以使用square
作為構(gòu)建塊來定義其他功能。 例如,我們可以輕松定義一個函數(shù)sum_squares
,給定任何兩個數(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的例子我們可以發(fā)現(xiàn),我們根本無法分辨square
是內(nèi)置在解釋器中,是從模塊導(dǎo)入的還是由用戶定義得。
def語句和賦值語句都是將名稱綁定到值,并且任何現(xiàn)有的綁定都將丟失。 例如,下文的g
首先指的是沒有參數(shù)的函數(shù),然后是一個數(shù)字,再然后是有兩個參數(shù)的另一個函數(shù)。
>>> def g():
return 1
>>> g()
1
>>> g = 2
>>> g
2
>>> def g(h, i):
return h + i
>>> g(1, 2)
3
1.3.1 環(huán)境
我們的Python子集現(xiàn)在已經(jīng)足夠復(fù)雜,但程序的含義還不是很明顯。 如果形式參數(shù)與內(nèi)建函數(shù)具有相同的名稱怎么辦呢? 兩個函數(shù)可以共享名稱嗎? 要解決這些問題,我們必須更詳細(xì)地描述環(huán)境。
表達(dá)式求值的環(huán)境由frame
幀的序列組成,它們可以被描述為一些盒子。 每個frame
都包含綁定,它們將名稱與其對應(yīng)的值相關(guān)聯(lián)。global frame
全局幀只有一個。 賦值和導(dǎo)入語句將條目添加到當(dāng)前環(huán)境的第一幀中。 到目前為止,我們的環(huán)境只包括全局幀。
>>> from math import pi
>>> tau = 2 * pi
環(huán)境圖示可以顯示出當(dāng)前環(huán)境的綁定,以及它們綁定的值。 您可以點(diǎn)擊Online Python Tutor鏈接,將示例加載到Online Python Tutor,這是由Philip Guo
創(chuàng)建的用于生成這些環(huán)境圖的工具。
函數(shù)也出現(xiàn)在環(huán)境圖中。 import
導(dǎo)入語句將名稱綁定到內(nèi)置函數(shù)。 def語句將名稱綁定到由用戶定義函數(shù)。 導(dǎo)入mul
和定義square
后的環(huán)境如下:
每個函數(shù)都是以
func
開頭的行,后跟函數(shù)名和形式參數(shù)。 內(nèi)建函數(shù)(如mul
)沒有形式參數(shù)名稱,因此總是使用。
函數(shù)的名稱重復(fù)兩次,一次在幀中,并再次作為函數(shù)本身的一部分。 函數(shù)中出現(xiàn)的名稱稱為intrinsic
內(nèi)在名稱。 幀中的名稱是綁定名稱。 兩者之間有區(qū)別:不同的名稱可能指的是相同的函數(shù),但該函數(shù)本身只有一個內(nèi)在名稱。
綁定到幀中的函數(shù)的名稱將會在求值過程中使用。 函數(shù)的內(nèi)在名稱在求值中不起作用。 通過下面的示例,一旦名稱max
被綁定到值3,它將不能再被用作為一個函數(shù)。
錯誤消息
TypeError
:'int' object is not callable
('int'對象不可調(diào)用)顯示名稱max
(當(dāng)前綁定到數(shù)字3)是一個整型而不是一個函數(shù)。 因此,它不能被用作調(diào)用表達(dá)式中的運(yùn)算符。
函數(shù)簽名。 函數(shù)因參數(shù)的數(shù)量不同。 用戶定義的函數(shù)square
只有一個參數(shù)x
; 提供更多或更少的參數(shù)將導(dǎo)致錯誤。 對函數(shù)的形式參數(shù)的描述被稱為函數(shù)的簽名。
函數(shù)max
可以有任意數(shù)量的參數(shù)。 它被渲染為max(...)
。 不管有多少個參數(shù),所有內(nèi)置函數(shù)將被呈現(xiàn)為<name>(...)。
1.3.2 調(diào)用用戶定義的函數(shù)
為了求出其操作符為用戶定義函數(shù)的調(diào)用表達(dá)式,Python解釋器遵循了以下計(jì)算過程。與任何調(diào)用表達(dá)式一樣,解釋器將對運(yùn)算符和操作數(shù)表達(dá)式求值,然后將具名函數(shù)應(yīng)用于生成的實(shí)參。
調(diào)用用戶定義的函數(shù)會引入第二個局部幀,它只能訪問該函數(shù)。為了對一些實(shí)參調(diào)用用戶定義的函數(shù):
1.在新的局部幀中,將實(shí)參綁定到的函數(shù)的形式參數(shù)上。
2.在以此幀開頭的環(huán)境中求出函數(shù)體。
函數(shù)體求值的環(huán)境由兩個幀組成:首先是包含形式參數(shù)綁定的局部幀,然后是包含其他所有內(nèi)容的全局幀。函數(shù)的每個實(shí)例都有自己的獨(dú)立局部幀。
為了詳細(xì)說明一個例子,下面描述了相同示例的環(huán)境圖的幾個步驟。執(zhí)行第一個import
語句后,只有名稱mul
被綁定在全局幀中。
首先,執(zhí)行函數(shù)
square
的定義語句。 請注意,整個def
語句在一個步驟中執(zhí)行。 直到調(diào)用函數(shù)才執(zhí)行函數(shù)體(不是在定義的時候)。接下來,使用參數(shù)-2調(diào)用
square
函數(shù),因此創(chuàng)建了一個新的幀,形式參數(shù)x
綁定到值-2
上。然后,在當(dāng)前環(huán)境中查找名稱
x
,它由所示的兩個幀組成。 在這兩種情況下,x
為-2,因此square
函數(shù)返回4。square()
的幀中的“return value”不是名稱綁定;而是指由創(chuàng)建該幀的函數(shù)調(diào)用所返回的值。
即使在這個簡單的例子中,也會使用兩種不同的環(huán)境。 我們在全局環(huán)境中計(jì)算最上方表達(dá)式square(-2)
,在通過調(diào)用square
創(chuàng)建的環(huán)境中計(jì)算返回表達(dá)式mul(x,x)
。x
和mul
都綁定在這個環(huán)境中,但是在不同的幀中。
環(huán)境中的幀順序會影響由表達(dá)式中名稱檢索而返回的值。 我們之前說過,名稱求解為與當(dāng)前環(huán)境中的該名稱相關(guān)聯(lián)的值。 我們現(xiàn)在可以更準(zhǔn)確地說:
我們關(guān)于環(huán)境,名稱和函數(shù)的概念建立了求值模型; 雖然一些機(jī)械細(xì)節(jié)仍然未敲定(例如如何實(shí)現(xiàn)綁定),但我們的模型能準(zhǔn)確而正確地描述解釋器如何求解調(diào)用表達(dá)式。 在第三章中,我們將看到這個模型如何作為藍(lán)圖來實(shí)現(xiàn)編程語言的工作解釋器。
1.3.3 示例:調(diào)用用戶定義的函數(shù)
讓我們再次考慮兩個簡單的函數(shù)定義,并說明用戶定義函數(shù)的調(diào)用表達(dá)式的求解過程。
Python首先求出名稱
sum_squares
,它綁定到了全局幀中的用戶定義的函數(shù)。 基本的數(shù)字表達(dá)式5和12求值為它們表達(dá)的數(shù)值。
接下來,Python調(diào)用了sum_squares
,它引入了將x
綁定到5和y
綁定到12的局部幀。
sum_squares
的函數(shù)體包含以下調(diào)用表達(dá)式:所有的三個子表達(dá)式在當(dāng)前環(huán)境中進(jìn)行求解,它開頭于標(biāo)記為
sum_squares()
的幀。 運(yùn)算符子表達(dá)式add
是在全局幀中找到的名稱,它綁定到內(nèi)建的加法函數(shù)中。 在調(diào)用加法函數(shù)之前,兩個操作數(shù)子表達(dá)式必須依次求值。 在當(dāng)前以標(biāo)記為sum_squares
的幀的環(huán)境中,對兩個操作數(shù)進(jìn)行求值。
在operand 0
中,square
命名了全局幀中的用戶定義的函數(shù),而x
則命名為局部幀的數(shù)字5。 Python通過引入另一個將x
綁定到5的局部幀來應(yīng)用square
到5。
在這種環(huán)境下,表達(dá)式mul(x,x)計(jì)算為25。
我們的求值過程現(xiàn)在輪到operand 1
,其中y
的值為12. Python會再次對square
的函數(shù)體進(jìn)行求解,此時引入另一個將x
綁定到12的局部幀。因此,operand 1
求值為144。
最后,對參數(shù)25和144調(diào)用加法得到
sum_squares
的最終返回值:169。這個例子說明了迄今為止我們發(fā)展出來的許多基本概念。 名稱綁定到值,這些值分布在許多獨(dú)立的局部幀,以及包含共享名稱的單個全局幀中。 每次調(diào)用一個函數(shù)時都會引入一個新的局部幀,即使是同一個函數(shù)被調(diào)用兩次的情況。
所有這些機(jī)制的存在,都是為了在程序執(zhí)行期間確保在正確的時間將名稱解析為正確的值。 這個例子說明了為什么我們的模型需要引入的復(fù)雜性。 所有三個局部幀都包含x
的綁定,但該名稱綁定到不同的幀中的不同值上。 局部幀分離了這些名稱。
1.3.4 局部名稱
函數(shù)實(shí)現(xiàn)的一個細(xì)節(jié)是實(shí)現(xiàn)者對函數(shù)的形式參數(shù)的名稱的選擇不應(yīng)影響函數(shù)行為。 因此,以下函數(shù)應(yīng)提供相同的行為:
>>> def square(x):
return mul(x, x)
>>> def square(y):
return mul(y, y)
這個原則 -- 函數(shù)應(yīng)該與其編寫者選擇的參數(shù)名稱無關(guān) --對編程語言有重要的意義。 最簡單的是函數(shù)的參數(shù)名稱必須保留在函數(shù)體的局部范圍內(nèi)。
如果參數(shù)不是它們各自函數(shù)主體的局部參數(shù),那么在square
中的參數(shù)x
可能與sum_squares
中的參數(shù)x
混淆。 嚴(yán)格來說,這并不是問題所在:在不同的局部幀中的x
綁定是不相關(guān)的。 我們的計(jì)算模型經(jīng)過嚴(yán)謹(jǐn)?shù)脑O(shè)計(jì),以確保這種獨(dú)立性。
局部名稱的作用范圍僅限于定義它的用戶定義函數(shù)體中。 當(dāng)一個名稱不能再被訪問時,它就離開了作用域。 這種作用域范圍界定行為不是我們模型的新事實(shí); 這是環(huán)境的工作方式的結(jié)果。
1.3.5 選擇名稱
名稱的可修改性并不意味著形式參數(shù)名稱不重要。相反,精心選擇的函數(shù)和參數(shù)名稱對于程序的可解釋性至關(guān)重要!
以下指導(dǎo)原則來自于Python代碼的樣式指南,它可以作為所有(非反叛)Python程序員的指南。這些共享的約定使開發(fā)者社區(qū)的成員之間的溝通能夠順利進(jìn)行。遵循這些約定有一些副作用,您將發(fā)現(xiàn)您的代碼在內(nèi)部變得一致。
1.函數(shù)名稱應(yīng)該小寫,用下劃線分隔。提倡描述性名稱。
2.函數(shù)名稱通常反映解釋器應(yīng)用于參數(shù)的操作(例如,print
,add
,square
)或結(jié)果(例如,max
,abs
,sum
)。
3.參數(shù)名稱應(yīng)該小寫,單詞用下劃線分隔。單字名稱是首選。
4.參數(shù)名稱應(yīng)該反映參數(shù)在函數(shù)中的作用。
5.當(dāng)作用明確時,單字參數(shù)名稱可以接受,但避免使用l
(小寫的L
)和O
(大寫的o
),或I
(大寫的i
)以避免與數(shù)字混淆。
這些指南也有許多例外,即使在Python標(biāo)準(zhǔn)庫中也是如此。像英語的詞匯一樣,Python繼承了各種貢獻(xiàn)者的詞匯,而結(jié)果并不總是一致的。
1.3.6 作為抽象的函數(shù)
盡管函數(shù)sum_squares
很簡單,但它可以說明用戶定義函數(shù)最強(qiáng)大的屬性。 函數(shù)sum_squares
是根據(jù)函數(shù)square
定義的,但僅依賴于square
的輸入?yún)?shù)與其輸出值之間的關(guān)系。
我們可以編寫sum_squares
,而不用考慮自己如何計(jì)算平方數(shù)。 平方數(shù)計(jì)算的細(xì)節(jié)被隱藏了,可以以后考慮。 事實(shí)上,就sum_squares
而言,square
并不是一個特定的函數(shù)體,而是某個函數(shù)的抽象。 在這個抽象層次上,任何能計(jì)算平方數(shù)的函數(shù)都是等價的。
因此,在只考慮返回值的情況下,以下兩個計(jì)算平方數(shù)的函數(shù)是難以區(qū)分的:它們都是接受數(shù)值參數(shù)并返回該數(shù)的平方值。
>>> def square(x):
return mul(x, x)
>>> def square(x):
return mul(x, x-1) + x
換句話說,函數(shù)定義能夠隱藏細(xì)節(jié)。函數(shù)的用戶可能沒有自己編寫功能,但從另一個程序員那里獲得它作為“黑盒子”。用戶只需要調(diào)用,不需要知道實(shí)現(xiàn)該功能的細(xì)節(jié)。 Python庫具有此屬性。許多開發(fā)人員使用這里定義的函數(shù),但很少有人去研究它們的實(shí)現(xiàn)。
函數(shù)式抽象。要掌握函數(shù)式抽象,需要考慮三個核心屬性。函數(shù)的域是它可以使用的參數(shù)集合;函數(shù)的范圍是返回值的集合;函數(shù)的功能是它在輸入和輸出之間的關(guān)系(以及它可能產(chǎn)生的任何副作用)。通過函數(shù)的域,范圍和意圖理解函數(shù)式抽象對于在復(fù)雜程序中正確使用它們至關(guān)重要。
例如,我們用于實(shí)現(xiàn)sum_squares
的任何平方函數(shù)應(yīng)具有以下屬性:
1.域是任意單個實(shí)數(shù)。
2.范圍是任意非負(fù)實(shí)數(shù)。
3.功能是輸出是輸入的平方。
這些屬性并沒有描述功能如何實(shí)現(xiàn)的細(xì)節(jié)部分,它們已經(jīng)被抽象了。
1.3.7 運(yùn)算符
算術(shù)運(yùn)算符(如+
和-
)在第一個例子中提供了組合方法,但是我們還沒有定義一個包含這些運(yùn)算符的表達(dá)式定義求值過程。
帶有中綴運(yùn)算符的Python表達(dá)式都有自己的求值過程,但是您經(jīng)??梢哉J(rèn)為它們是調(diào)用表達(dá)式的快捷方式。 當(dāng)您看到
>>> 2 + 3
5
的時候,可以簡單地認(rèn)為它是
>>> add(2, 3)
5
的快捷方式。中綴符號可以嵌套,就像調(diào)用表達(dá)式一樣。 Python運(yùn)算符優(yōu)先級采用了常規(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還允許使用括號對子表達(dá)式進(jìn)行分組,以覆蓋通常的優(yōu)先級規(guī)則,或使表達(dá)式的嵌套結(jié)構(gòu)更加明顯。
>>> (2 + 3) * (4 + 5)
45
它和以下表達(dá)式的求解結(jié)果完全相同
>>> mul(add(2, 3), add(4, 5))
45
對于除法,Python提供了兩個中綴運(yùn)算符:/
和//
。 前者是常規(guī)除法,及時是整除,結(jié)果也是浮點(diǎn)數(shù):
>>> 5 / 4
1.25
>>> 8 / 4
2.0
后一個運(yùn)算符//
,直接將結(jié)果舍入到一個整數(shù)
>>> 5 // 4
1
>>> -5 // 4
-2
這兩個運(yùn)算符是對truediv
和floordiv
函數(shù)的快捷方式。
>>> from operator import truediv, floordiv
>>> truediv(5, 4)
1.25
>>> floordiv(5, 4)
1
您應(yīng)該在程序中自由使用中綴操作符和括號。對于簡單的算術(shù)運(yùn)算,Python在慣例上傾向于使用運(yùn)算符而不是調(diào)用表達(dá)式。