來源:1.3 Defining New Functionshtml
譯者:飛龍python
協議:CC BY-NC-SA 4.0git
咱們已經在 Python 中認識了一些在任何強大的編程語言中都會出現的元素:程序員
數值是內建數據,算數運算是函數。github
嵌套函數提供了組合操做的手段。express
名稱到值的綁定提供了有限的抽象手段。編程
如今咱們將要了解函數定義,一個更增強大的抽象技巧,名稱經過它能夠綁定到複合操做上,並能夠做爲一個單元來引用。框架
咱們經過如何表達「平方」這個概念來開始。咱們可能會說,「對一個數求平方就是將這個數乘上它本身」。在 Python 中就是:編程語言
>>> def square(x): return mul(x, x)
這定義了一個新的函數,並賦予了名稱square
。這個用戶定義的函數並不內建於解釋器。它表示將一個數乘上本身的複合操做。定義中的x
叫作形式參數,它爲被乘的東西提供一個名稱。這個定義建立了用戶定義的函數,而且將它關聯到名稱square
上。函數
函數定義包含def
語句,它標明瞭<name>
(名稱)和一列帶有名字的<formal parameters>
(形式參數)。以後,return
(返回)語句叫作函數體,指定了函數的<return expression>
(返回表達式),它是函數不管何時調用都須要求值的表達式。
def <name>(<formal parameters>): return <return expression>
第二行必須縮進!按照慣例咱們應該縮進四個空格,而不是一個Tab,返回表達式並非當即求值,它儲存爲新定義函數的一部分,而且只在函數最終調用時會被求出。(很快咱們就會看到縮進區域能夠跨越多行。)
定義了square
以後,咱們使用調用表達式來調用它:
>>> square(21) 441 >>> square(add(2, 5)) 49 >>> square(square(3)) 81
咱們也能夠在構建其它函數時,將square
用做構建塊。列入,咱們能夠輕易定義sum_squares
函數,它接受兩個數值做爲參數,並返回它們的平方和:
>>> def sum_squares(x, y): return add(square(x), square(y)) >>> sum_squares(3, 4) 25
用戶定義的函數和內建函數以同種方法使用。確實,咱們不可能在sum_squares
的定義中分辨出square
是否構建於解釋器中,從模塊導入仍是由用戶定義。
咱們的 Python 子集已經足夠複雜了,但程序的含義還不是很是明顯。若是形式參數和內建函數具備相同名稱會如何呢?兩個函數是否能共享名稱而不會產生混亂呢?爲了解決這些疑問,咱們必須詳細描述環境。
表達式求值所在的環境由幀的序列組成,它們能夠表述爲一些盒子。每一幀都包含了一些綁定,它們將名稱和對應的值關聯起來。全局幀只有一個,它包含全部內建函數的名稱綁定(只展現了abs
和max
)。咱們使用地球符號來表示全局。
賦值和導入語句會向當前環境的第一個幀添加條目。到目前爲止,咱們的環境只包含全局幀。
>>> from math import pi >>> tau = 2 * pi
def
語句也將綁定綁定到由定義建立的函數上。定義square
以後的環境如圖所示:
這些環境圖示展現了當前環境中的綁定,以及它們所綁定的值(並非任何幀的一部分)。要注意函數名稱是重複的,一個在幀中,另外一個是函數的一部分。這一重複是有意的,許多不一樣的名字可能會引用相同函數,可是函數自己只有一個內在名稱。可是,在環境中由名稱檢索值只檢查名稱綁定。函數的內在名稱不在名稱檢索中起做用。在咱們以前看到的例子中:
>>> f = max >>> f <built-in function max>
名稱max
是函數的內在名稱,以及打印f
時咱們看到的名稱。此外,名稱max
和f
在全局環境中都綁定到了相同函數上。
在咱們介紹 Python 的附加特性時,咱們須要擴展這些圖示。每次咱們這樣作的時候,咱們都會列出圖示能夠表達的新特性。
新的環境特性:賦值和用戶定義的函數定義。
爲了求出運算符爲用戶定義函數的調用表達式,Python 解釋器遵循與求出運算符爲內建函數的表達式類似的過程。也就是說,解釋器求出操做數表達式,而且對產生的實參調用具名函數。
調用用戶定義的函數的行爲引入了第二個局部幀,它只能由函數來訪問。爲了對一些實參調用用戶定義的函數:
在新的局部幀中,將實參綁定到函數的形式參數上。
在當前幀的開頭以及全局幀的末尾求出函數體。
函數體求值所在的環境由兩個幀組成:第一個是局部幀,包含參數綁定,以後是全局幀,包含其它全部東西。每一個函數示例都有本身的獨立局部幀。
這張圖包含兩個不一樣的 Python 解釋器層面:當前的環境,以及表達式樹的一部分,它和要求值的代碼的當前一行相關。咱們描述了調用表達式的求值,用戶定義的函數(藍色)表示爲兩部分的圓角矩形。點線箭頭表示哪一個環境用於在每一個部分求解表達式。
上半部分展現了調用表達式的求值。這個調用表達式並不在任何函數裏面,因此他在全局環境中求值。因此,任何裏面的名稱(例如square
)都會在全局幀中檢索。
下半部分展現了square
函數的函數體。它的返回表達式在上面的步驟1引入的新環境中求值,它將square
的形式參數x
的名稱綁定到實參的值-2
上。
環境中幀的順序會影響由表達式中的名稱檢索返回的值。咱們以前說名稱求解爲當前環境中與這個名稱關聯的值。咱們如今能夠更精確一些:
名稱求解爲當前環境中,最早發現該名稱的幀中,綁定到這個名稱的值。
咱們關於環境、名稱和函數的概念框架創建了求值模型,雖然一些機制的細節仍舊沒有指明(例如綁定如何實現),咱們的模型在描述解釋器如何求解調用表示上,變得更準確和正確。在第三章咱們會看到這一模型如何用做一個藍圖來實現編程語言的可工做的解釋器。
新的環境特性:函數調用。
讓咱們再一次考慮兩個簡單的定義:
>>> from operator import add, mul >>> def square(x): return mul(x, x) >>> def sum_squares(x, y): return add(square(x), square(y))
以及求解下列調用表達式的過程:
>>> sum_squares(5, 12) 169
Python 首先會求出名稱sum_squares
,它在全局幀綁定了用戶定義的函數。基本的數字表達式 5 和 12 求值爲它們所表達的數值。
以後,Python 調用了sum_squares
,它引入了局部幀,將x
綁定爲 5,將y
綁定爲 12。
這張圖中,局部幀指向它的後繼,全局幀。全部局部幀必須指向某個先導,這些連接定義了當前環境中的幀序列。
sum_square
的函數體包含下列調用表達式:
add ( square(x) , square(y) ) ________ _________ _________ "operator" "operand 0" "operand 1"
所有三個子表達式在當前環境中求值,它開始於標記爲sum_squares
的幀。運算符字表達式add
是全局幀中發現的名稱,綁定到了內建的加法函數上。兩個操做數子表達式必須在加法函數調用以前依次求值。兩個操做數都在當前環境中求值,開始於標記爲sum_squares
的幀。在下面的環境圖示中,咱們把這一幀叫作A
,而且將指向這一幀的箭頭同時替換爲標籤A
。
在使用這個局部幀的狀況下,函數體表達式mul(x, x)
求值爲 25。
咱們的求值過程如今輪到了操做數 1,y
的值爲 12。Python 再次求出square
的函數體。此次引入了另外一個局部環境幀,將x
綁定爲 12。因此,操做數 1 求值爲 144。
最後,對實參 25 和 144 調用加法會產生sum_squares
函數體的最終值:169。
這張圖雖然複雜,可是用於展現咱們目前爲止發展出的許多基礎概念。名稱綁定到值上面,它延伸到許多局部幀中,局部幀在惟一的全局幀之上,全局幀包含共享名稱。表達式爲樹形結構,以及每次子表達式包含用戶定義函數的調用時,環境必須被擴展。
全部這些機制的存在確保了名稱在表達式中正確的地方解析爲正確的值。這個例子展現了爲何咱們的模型須要所引入的複雜性。全部三個局部幀都包含名稱x
的綁定。可是這個名稱在不一樣的幀中綁定到了不一樣的值上。局部幀分離了這些名稱。
函數實現的細節之一是實現者對形式參數名稱的選擇不該影響函數行爲。因此,下面的函數應具備相同的行爲:
>>> def square(x): return mul(x, x) >>> def square(y): return mul(y, y)
這個原則 -- 也就是函數應不依賴於編寫者選擇的參數名稱 -- 對編程語言來講具備重要的結果。最簡單的結果就是函數參數名稱應保留在函數體的局部範圍中。
若是參數不位於相應函數的局部範圍中,square
的參數x
可能和sum_squares
中的參數x
產生混亂。嚴格來講,這並非問題所在:不一樣局部幀中的x
的綁定是不相關的。咱們的計算模型具備嚴謹的設計來確保這種獨立性。
咱們說局部名稱的做用域被限制在定義它的用戶定義函數的函數體中。當一個名稱不能再被訪問時,它就離開了做用域。做用域的行爲並非咱們模型的新事實,它是環境的工做方式的結果。
可修改的名稱並不表明形式參數的名稱徹底不重要。反之,選擇良好的函數和參數名稱對於函數定義的人類可解釋性是必要的。
下面的準則派生於 Python 的代碼風格指南,可被全部(非反叛)Python 程序員做爲指南。一些共享的約定會使社區成員之間的溝通變得容易。遵循這些約定有一些反作用,我會發現你的代碼在內部變得一致。
函數名稱應該小寫,如下劃線分隔。提倡描述性的名稱。
函數名稱一般反映解釋器向參數應用的操做(例如print
、add
、square
),或者結果(例如max
、abs
、sum
)。
參數名稱應小寫,如下劃線分隔。提倡單個詞的名稱。
參數名稱應該反映參數在函數中的做用,並不只僅是知足的值的類型。
看成用很是明確時,單個字母的參數名稱能夠接受,可是永遠不要使用l
(小寫的L
)和O
(大寫的o
),或者I
(大寫的i
)來避免和數字混淆。
週期性對你編寫的程序複查這些準則,不用多久你的名稱會變得十分 Python 化。
雖然sum_squares
十分簡單,可是它演示了用戶定義函數的最強大的特性。sum_squares
函數使用square
函數定義,可是僅僅依賴於square
定義在輸入參數和輸出值之間的關係。
咱們能夠編寫sum_squares
,而不用考慮如何計算一個數值的平方。平方計算的細節被隱藏了,並能夠在以後考慮。確實,在sum_squares
看來,square
並非一個特定的函數體,而是某個函數的抽象,也就是所謂的函數式抽象。在這個層級的抽象中,任何能計算平方的函數都是等價的。
因此,僅僅考慮返回值的狀況下,下面兩個計算平方的函數是難以區分的。每一個都接受數值參數而且產生那個數的平方做爲返回值。
>>> def square(x): return mul(x, x) >>> def square(x): return mul(x, x-1) + x
換句話說,函數定義應該可以隱藏細節。函數的用戶可能不能本身編寫函數,可是能夠從其它程序員那裏得到它做爲「黑盒」。用戶不該該須要知道如何實現來調用。Python 庫擁有這個特性。許多開發者使用在這裏定義的函數,可是不多有人看過它們的實現。實際上,許多 Python 庫的實現並不徹底用 Python 編寫,而是 C 語言。
算術運算符(例如+
和-
)在咱們的第一個例子中提供了組合手段。可是咱們還須要爲包含這些運算符的表達式定義求值過程。
每一個帶有中綴運算符的 Python 表達式都有本身的求值過程,可是你一般能夠認爲他們是調用表達式的快捷方式。當你看到
>>> 2 + 3 5
的時候,能夠簡單認爲它是
>>> add(2, 3) 5
的快捷方式。
中綴記號能夠嵌套,就像調用表達式那樣。Python 運算符優先級中採用了常規的數學規則,它指導瞭如何解釋帶有多種運算符的複合表達式。
>>> 2 + 3 * 4 + 5 19
和下面的表達式的求值結果相同
>>> add(add(2, mul(3, 4)) , 5) 19
調用表達式的嵌套比運算符版本更加明顯。Python 也容許括號括起來的子表達式,來覆蓋一般的優先級規則,或者使表達式的嵌套結構更加明顯:
>>> (2 + 3) * (4 + 5) 45
和下面的表達式的求值結果相同
>>> mul(add(2, 3), add(4, 5)) 45
你應該在你的程序中自由使用這些運算符和括號。對於簡單的算術運算,Python 在慣例上傾向於運算符而不是調用表達式。