Python中的函式詳解

PyPer發表於2015-06-27

Python中的函式,無論是命名函式,還是匿名函式,都是語句和表示式的集合。在Python中,函式是第一個類物件,這意味著函式的用法並沒有限制。Python函式的使用方式就像Python中其他值一樣,例如字串和數字等。Python函式擁有一些屬性,通過使用Python內建函式dir就能檢視這些屬性,如下程式碼所示:

其中,一些重要的函式屬性包括以下幾個:

1. __doc__返回指定函式的文件字串。

2. __name__返回函式名字。

3. __module__返回函式定義所在模組的名字。

4. func_defaults返回一個包含預設引數值的元組,預設引數將在後文進行討論。

5. func_globals返回一個包含函式全域性變數的字典引用。

6. func_dict返回支援任意函式屬性的名稱空間。

7. func_closure返回一個胞體元組,其中胞體包含了函式自由變數的繫結,閉包將在後文討論。

函式可以作為引數傳遞給其他函式。這些以其他函式作為引數的函式通常稱為更高階函式,這就構成了函數語言程式設計中一個非常重要的部分。高階函式一個很好的例子就是map函式,該函式接受一個函式和一個迭代器作為引數,並將函式應用於迭代器中的每一項,最後返回一個新的列表。我們將在下面的例子中演示這一點,例子中將前面定義的square函式和一個數字迭代器傳遞給map函式。

此外,函式也可以在其他函式程式碼塊內部定義,同時也能從其他函式呼叫中返回。

在上面的例子中,我們在函式outer中定義了另一個函式inner,並且當函式outer執行時將返回inner函式。此外,像任何其他Python物件一樣,函式也可以賦值給變數,如下所示:

在上面的例子中,outer函式被呼叫時將會返回一個函式,並將返回的函式賦值給變數func。最後,該變數就可以像被返回的函式一樣被呼叫:

函式定義

關鍵字def用於建立使用者自定義函式,函式定義就是一些可執行的語句。

在上面的square函式中,當包含該函式的模組載入到Python直譯器中時,或者如果該函式在Python REPL中定義,那麼將會執行函式定義語句def square(x)。然而,這對以可變資料結構作為值的預設引數有一些影響,這一點我們將會在後文講述。函式定義的執行會繫結當前本地名稱空間中的函式名(可以將名稱空間當作名字到值的一種對映,並且這種對映還可以巢狀,名稱空間和範圍會在另一個教程中詳細介紹)到一個函式物件,該物件是一個對函式中可執行程式碼的包裝器。這個函式物件包含了一個對當前全域性名稱空間的引用,而當前名稱空間指該函式呼叫時所使用的全域性名稱空間。此外,函式定義不會執行函式體,只有在函式被呼叫時才會執行函式體。

函式呼叫引數

除了正常的引數之外,Python函式還支援數量可變的引數。這些引數有主要有下面描述的三種類別:

1. 預設引數值:這允許使用者為函式的引數定義一些預設值。這種情況下,可以以更少的引數來呼叫該函式,而函式呼叫時未提供的引數,Python會使用預設提供的值作為這些引數值。下面的例子展示了這種用法:

上面例子函式的定義中,包含一個正常位置的引數arg和兩個預設引數def_arg和def_arg2。該函式可以以下面中的任何一種方式進行呼叫:

(1)只提供非預設位置引數值。在本例中,預設引數取預設值:

(2)用提供的值覆蓋一些預設的引數值,包括非預設位置引數:

(3)為所有引數提供值,可以用這些值覆蓋預設引數值:

當使用可變的預設資料結構作為預設引數時,需要特別小心。因為函式定義只執行一次,所以這些可變的資料結構(引用值)只在函式定義時建立一次。這就意味著,相同的可變資料結構將用於所有函式呼叫,如下面例子所示:

在每個函式呼叫中,“Hello World”都被新增到了def_arg列表中,在呼叫兩次函式之後,預設引數中將有兩個“Hello World”字串。當使用可變預設引數作為預設值時,注意到這一點非常重要。當我們討論Python資料模型時,將會清楚理解其原因。

2. 關鍵字引數:以“kwarg=value”的形式使用關鍵字引數也可以呼叫函式。其中,kwarg指函式定義中使用的引數名稱。以下面定義的含有預設和非預設引數的函式為例:

為了演示使用關鍵字引數呼叫函式,下面的函式可以以後面的任何一種方式呼叫:

在函式呼叫中,關鍵字引數不得早於非關鍵字引數,所以以下呼叫會失敗:

函式不能為一個引數提供重複值,所以下面的呼叫方法是非法的:

在上面的例子中,引數arg是位置引數,所以值“test”會分配給它。而試圖將其再次分配給關鍵字arg,意味著在嘗試多重賦值,而這是非法的。

傳遞的所有關鍵字引數必須匹配一個函式接受的引數,而包含非可選引數的關鍵字順序並不重要,所以下面調換了引數順序的寫法是合法的:

3. 任意的引數列表:Python還支援定義這樣的函式,該函式可以接受以元組形式傳遞的任意數量的引數,Python教程中的一個例子如下所示:

任意數量的引數必須在正常引數之後。在本例中,任意數量引數存在於引數file和separator之後。下面是一個呼叫上述定義函式的示例:

上面的引數one、two、three、four、five捆綁在一起共同組成了一個元組,通過引數args就能訪問該元組。

解包函式引數

有時候,函式呼叫的引數可能是以元組、列表或字典的形式存在。可以通過使用“*”或“**”操作符將這些引數解包到函式內部以供呼叫。以下面的函式為例,該函式接受兩個位置引數,並列印出兩個引數的值。

如果提供給函式的引數值是以列表形式存在,那麼我們可以直接將這些值解包到函式中,如下所示:

類似的,當我們有關鍵詞時,可以使用字典來儲存kwarg到值的對映關係,並利用“**”操作符將關鍵字引數解包到函式,如下所示:

利用“*”和“**”定義函式

有時候,當定義一個函式時,我們之前可能不知道引數的數量。這就導致了下面簽名的函式定義:

“*args”參數列示未知的位置引數序列長度,而“**kwargs”代表包含關鍵字和值對映關係的字典,它可以包含任意數量的關鍵字和值對映,並且在函式定義中“*args”必須位於“**kwargs”前面。下面的程式碼演示了這種情況:

必須向函式提供正常的引數,但“*args”和“**kwargs”卻是可選的,如下所示:

在函式呼叫中,普通引數以正常方式提供,而可選引數則可以通過解包的形式到達函式呼叫中。

匿名函式

Python也支援匿名函式,這些函式使用lambda關鍵字建立。Python中Lambda表示式的形式如下所示:

Lambda表示式返回評估後的函式物件,並且具有與命名函式相同的屬性。在Python中,Lambda表示式通常只用於非常簡單的函式,如下所示:

上面的lambda表示式的功能與下面命名函式的功能相同:

巢狀函式和閉包

在一個函式內部定義函式就建立了巢狀函式,如下所示:

在這種型別的函式定義中,函式inner只在函式outer內部有效,所以當內部函式需要被返回(移動到外部作用範圍)或被傳遞給另一個函式時,使用巢狀函式通常比較方便。在如在上面的巢狀函式中,每次呼叫外部函式時都會建立一個新的巢狀函式例項,這是因為,在每次執行外部函式時,都會執行一次內部函式定義,而其函式體則不會被執行。

巢狀函式可以訪問建立它的環境,這是python函式定義語義的直接結果。一個結果是,外部函式中定義的變數可以在內部函式中引用,即使外部函式已經執行結束。

當內部巢狀的函式引用外部函式中的變數時,我們說巢狀函式相對於引用變數是封閉的。我們可以使用函式物件的一個特殊屬性“__closure__”來訪問這個封閉的變數,如下所示:

Python中的閉包有一個古怪的行為。在Python 2.x及更低版本中,指向不可變型別(例如字串和數字)的變數不能在閉包內反彈。下面的例子說明了這一點:

一個相當不可靠的解決方案是,使用一個可變型別來捕獲閉包,如下所示:

Python 3引入了“nonlocal”關鍵字用來解決下面所示的閉包範圍問題。在本教程名稱空間一節中,我們更加詳細地描述了這些古怪用法。

閉包可以用來維持狀態(與類作用不同),在一些簡單的情況下,還可以提供一種簡潔性與可讀性比類更強的解決方案,我們使用tech_pro中的一個日誌例子來說明這一點。假設一個非常簡單的日誌API,它使用基於類的物件導向思想,並可以在不同級別上列印日誌:

相同的功能也可以使用閉包來實現,如下所示:

可以看出,即使兩個版本都實現了相同的功能,但基於閉包的版本更簡潔、可讀性更好。閉包在一個主要的Python函式“函式修飾符”中也扮演著很重要的角色,這是使用非常廣泛的功能,我們將在接下來的教程講解。

如果文中你發現了任何錯誤、問題或者你有更好的話題想讓我寫出來,可以在Twitter上聯絡我(@obi_inc)。

擴充套件閱讀

相關文章