Python必學基礎知識之函數

本章主要內容

  • 定義函數
  • 使用函數參數
  • 用可變對象作爲參數
  • 理解局部變量和全局變量
  • 創建和使用生成器函數
  • 創建和使用lambda表達式
  • 使用裝飾器

本章假定讀者至少熟悉另一種計算機語言的函數定義方法,包括函數定義、實參(argument)和形參(parameter)等概念。

9.1 基本的函數定義

Python函數定義的基本語法如下:

def name(parameter1, parameter2, . . .):
    body

與代碼流程控制結構一樣,Python用縮進來界定函數體。以下示例將之前計算階乘的代碼放入函數體中,這樣只需調用fact函數即可得到階乘值了:

>>> def fact(n):
...     """ Return the factorial of the given number. """    ⇽---  ❶
...     r = 1
...     while n > 0:
...         r = r * n
...         n = n - 1
...     return r           ⇽---  ❶
...

第二行❶是可選的文檔字符串(docstring),可通過fact.__doc__讀取其值。文檔字符串用於描述函數對外表現出來的功能及所需的參數,而註釋(comment)則是記錄代碼工作原理的內部信息。文檔字符串緊隨在函數定義的第一行後面,通常用3重引號包圍起來,以便能跨越多行。代碼助手只會提取文檔字符串的第一行。標準的多行文檔字符串寫法,是在第一行中給出函數的概述,第二行是空行,然後是其餘的詳細信息。return語句之後的值將會返回給函數的調用者❷。

過程與函數

 

在某些編程語言中,無返回值的函數被稱爲“過程”。雖然Python允許編寫不含return語句的函數,但這些函數還不是真正的過程。所有的Python過程都是函數。如果過程體沒有顯式地執行return語句,則會返回特殊值None。如果執行了return arg語句,則值arg會被立即返回。return語句執行之後,函數體中的其餘語句都不會執行。因爲Python沒有真正的過程,所以均被稱爲“函數”。

雖然Python函數都帶有返回值,但是否使用這個返回值則由寫代碼的人決定:

>>> fact(4)     ⇽---  ❶
24     ⇽---  ❷
>>> x = fact(4)     ⇽---  ❸
>>> x
24
>>>

一開始返回值沒有與任何變量關聯❶,fact函數的值只是被解釋器打印出來而已❷。然後返回值與變量x關聯❸。

9.2 多種函數參數

大多數函數都需要形參,每種編程語言都有各自的函數形參定義規則。Python非常靈活,提供了3種函數形參的定義方式。本節將介紹這些定義方式。

9.2.1 按位置給出形參

在Python中,最簡單的函數傳形參方式就是按位置給出。在函數定義的第一行中,可以爲每個形參指定變量名稱。當調用函數時,調用代碼中給出的形參將按順序與函數的形參變量逐一匹配。以下函數計算x的y次冪:

>>> def power(x, y):
...     r = 1
...     while y > 0:
...         r = r * x
...         y = y - 1
...     return r

>>> power(3, 3)
27

上述用法要求,調用代碼使用的形參數量與函數定義時的形參數量應完全匹配,否則會引發TypeError:

>>> power(3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: power() missing 1 required positional argument: 'y'
>>>

默認值

 

函數的形參可以有默認值,可以在函數定義的第一行中給出該默認值,如下所示:

def fun(arg1, arg2=default2, arg3=default3, ...)

可以爲任何數量的形參給出默認值。帶默認值的形參必須位於形參列表的末尾,因爲與大多數編程語言一樣,Python也是根據位置來把實參與形參匹配起來的。給函數的實參數量必須足夠多,以便讓形參列表中最後一個不帶默認值的形參能獲取到實參。更爲靈活的機制參見9.2.2節。

以下函數同樣也會計算x的y次冪。但如果在函數調用時沒有給出y,則會用默認值2,於是就成了計算平方的函數:

>>> def power(x, y=2):
...     r = 1
...     while y > 0:
...         r = r * x
...         y = y - 1
...     return r

以下交互式會話演示了默認實參的效果:

>>> power(3, 3)
27
>>> power(3)
9

9.2.2 按形參名稱傳遞實參

也可以使用對應的函數形參的名稱將實參傳給函數,而不是按照形參的位置給出。繼續上面的交互示例,可以鍵入

>>> power(2, 3) 
8
>>> power(3, 2)
9
>>> power(y=2, x=3)
9

最後提交給power函數的實參帶了名稱,因此與順序無關。實參與power函數定義中的同名形參關聯起來,得到的是3^2的結果。這種實參傳遞方式被稱爲關鍵字傳遞(keyword passing)。

如果函數需要帶有大量實參,並且大多數實參都有默認值,那麼聯合使用關鍵字傳遞和默認實參功能可能就非常有用了。例如,有個生成當前目錄下文件信息清單的函數,可用布爾型實參指定清單中是否要包含每個文件的大小、最後修改日期等信息。函數定義如下所示:

def list_file_info(size=False, create_date=False, mod_date=False, ... ):
    ...獲取文件名...  
    if size:
        # 獲取文件大小
    if create_date:
        # 獲取文件的創建日期
    # 其他功能
    return fileinfostructure

然後用關鍵字傳遞方式調用,指明需要包含的文件信息(在本例中爲文件大小和修改日期,但不是創建日期):

fileinfo = list_file_info( size = True,mod_date = True)

這種參數處理方式特別適用於非常複雜的函數,圖形用戶界面(GUI)中常會用到。如果用過Tkinter包建立Python的GUI程序,就會發現這種可選的關鍵字命名實參是非常有用的。

9.2.3 變長實參

Python函數也可以定義爲實參數量可變的形式,定義方式有兩種。一種用於處理實參預期相對明瞭的情況,實參列表尾部數量不定的實參將會被放入一個列表中。另一種方式可將任意數量的關鍵字傳遞實參放入一個字典中,這些實參均是在函數形參列表中不存在同名形參的。下面將介紹這兩種機制。

1.位置實參數量不定時的處理

當函數的最後一個形參名稱帶有“*”前綴時,在一個函數調用中所有多出來的非關鍵字傳遞實參(即這些按位置給出的實參未能賦給合適的形參)將會合併爲一個元組賦給該形參。下面用這種簡單方式來實現一個求數字列表中最大值的函數。

首先,實現函數:

>>> def maximum(*numbers):
...     if len(numbers) == 0:
...         return None
...     else:
...         maxnum = numbers[0]
...         for n in numbers[1:]:
...             if n > maxnum:
...                 maxnum = n
...         return maxnum
...

接下來,測試該函數的功能:

>>> maximum(3, 2, 8)
8
>>> maximum(1, 5, 9, -2, 2)
9

2.關鍵字傳遞實參數量不定時的處理

按關鍵字傳遞的實參數量不定時,也能進行處理。如果形參列表的最後一個形參前綴爲“**”,那麼所有多餘的關鍵字傳遞實參將會被收入一個字典對象中。字典的鍵爲多餘實參的關鍵字(形參名稱),字典的值爲實參本身。這裏的“多餘”是指,傳遞實參的關鍵字匹配不到函數定義中的形參名稱。

例如:

>>> def example_fun(x, y, **other):
...     print("x: {0}, y: {1}, keys in 'other': {2}".format(x, 
...           y, list(other.keys())))
...     other_total = 0
...     for k in other.keys():
...         other_total = other_total + other[k]
...     print("The total of values in 'other' is {0}".format(other_total))

在交互會話中測試一下,以上函數可以處理用關鍵字foo和bar傳入的實參,即便foo和bar不屬於函數定義中給出的形參名也沒問題:

>>> example_fun(2, y="1", foo=3, bar=4)
x: 2, y: 1, keys in 'other': ['foo', 'bar']
The total of values in 'other' is 7

9.2.4 多種參數傳遞方式的混用

Python函數的所有實參傳遞方式可以同時使用,但一不小心就可能會引起混亂。混合使用多種實參傳遞方式的一般規則是,先按位置傳遞實參,接着是命名實參,然後是帶單個“*”的數量不定的位置傳遞實參,最後是帶“**”的數量不定的關鍵字傳遞實參。詳細信息參見官方文檔。

速測題:函數和參數 該如何編寫函數,接收任意數量的未命名實參,並逆序打印出來?

如何創建過程,也就是無返回值的函數?

如果用變量捕獲函數的返回值,會發生什麼?

9.3 將可變對象用作函數實參

函數的實參傳遞的是對象的引用,形參則成爲指向對象的新引用。對於不可變對象(如元組、字符串和數值),對形參的操作不會影響函數外部的代碼。但如果傳入的是可變對象(如列表、字典或類的實例),則對該對象做出的任何改動都會改變該實參在函數外引用的值。函數內部對形參的重新賦值不會影響實參,如圖9-1和圖9-2所示:

>>> def f(n, list1, list2):
...     list1.append(3)
...     list2 = [4, 5, 6]
...     n = n + 1
...
>>> x = 5
>>> y = [1, 2]
>>> z = [4, 5]
>>> f(x, y, z)
>>> x, y, z
(5, [1, 2, 3], [4, 5])

Python必學基礎知識之函數

 

圖9-1 在函數f()開始執行時,各初始變量和函數形參分別都指向同一個對象

Python必學基礎知識之函數

 

圖9-2 在函數f()執行完畢後,y(函數內的list1)引用的值已經發生了變化,而n和list2則指向了不同的對象

圖9-1和圖9-2演示了調用函數f時發生的事情。變量x沒有變化,因爲x是不可變的。而函數形參n則被指向了新的值6。同理,變量z沒有變化,因爲在函數f內,對應的形參list2被指向了新的對象[4,5,6]。只有y發生了變化,因爲其指向的實際列表發生了變化。

速測題:函數參數爲可變類型 如果將列表或字典作爲參數值傳入函數,那麼(在函數內)對其進行修改會導致什麼結果?哪些操作可能會導致改動對函數外部也是可見的?可採取什麼措施降低這種改動風險?

9.4 局部變量、非局部變量和全局變量

下面回顧一下本章開始介紹過的fact函數的定義:

>>> def fact(n):
        """ 返回給定值的階乘 """
        r = 1
        while n > 0:
            r = r * n
            n = n - 1
        return r

變量r和n對於fact函數的任何調用都是局部(local)的,在函數執行期間,它們的變化對函數外部的任何變量都沒有影響。函數形參列表中的所有變量,以及通過賦值(如fact函數中的r = 1)在函數內部創建的所有變量,都是該函數的局部變量。

在使用變量之前,用global語句對其進行聲明,可以顯式地使其成爲全局(global)變量。函數可以訪問和修改全局變量。全局變量存在於函數之外,所有將其聲明爲全局變量的其他函數,以及函數之外的代碼,也可以對其進行訪問和修改。以下示例演示了局部變量和全局變量的差異:

>>> def fun():
...     global a
...     a = 1
...     b = 2
...

以上示例中定義的函數,將a視爲全局變量,而視b爲局部變量,並對a和b進行了修改。

下面測試一下上述函數:

>>> a = "one"
>>> b = "two"

>>> fun()
>>> a
1
>>> b
'two'

在fun函數內對a的賦值,同時也是對fun函數外部現存的全局變量a進行操作。因爲a在fun函數中被指定爲global,所以賦值會將該全局變量從"one"修改爲1。對b來說則不一樣,fun函數內部名爲b的局部變量一開始指向fun函數外部的變量b的相同值[1],但賦值操作讓b指向了函數fun內的新值。

nonlocal語句與global語句類似,它會讓標識符引用最近的閉合作用域(enclosing scope)中已綁定的變量。第10章中將會更詳細地介紹作用域和命名空間,現在的重點是要理解,global語句是對頂級變量使用的,而nonlocal語句則可引用閉合作用域中的全部變量,如代碼清單9-1所示。

代碼清單9-1 nonlocal.py文件

g_var = 0          ⇽---  inner_test函數中的g_var綁定爲同名的頂級變量
nl_var = 0
print("top level-> g_var: {0} nl_var: {1}".format(g_var, nl_var))
def test(): 
    nl_var = 2     ⇽---  inner_test函數中的nl_ var綁定爲test函數中的同名變量
    print("in test-> g_var: {0} nl_var: {1}".format(g_var, nl_var))
    def inner_test():
        global g_var   ⇽---  inner_test函數中的g_var綁定爲同名的頂級變量
        nonlocal nl_var   ⇽---  inner_test函數中的nl_var綁定爲test函數中的同名變量
        g_var = 1
        nl_var = 4
        print("in inner_test-> g_var: {0} nl_var: {1}".format(g_var,
            nl_var))

    inner_test()
    print("in test-> g_var: {0} nl_var: {1}".format(g_var, nl_var))

test()
print("top level-> g_var: {0} nl_var: {1}".format(g_var, nl_var))

上述代碼運行後會打印出以下結果:

top level-> g_var: 0 nl_var: 0
in test-> g_var: 0 nl_var: 2
in inner_test-> g_var: 1 nl_var: 4
in test-> g_var: 1 nl_var: 4
top level-> g_var: 1 nl_var: 0

注意,頂級變量nl_var的值沒有受到影響。如果inner_test函數中包含global nl_var語句,那麼nl_var的值就會受影響了。

最起碼的一點就是,如果想對函數之外的變量賦值,就必須將其顯式聲明爲nonlocal或global。但如果只是要訪問函數外的變量,則不需要將其聲明爲nonlocal或global。如果Python在函數本地作用域中找不到某變量名,就會嘗試在全局作用域中查找。因此,對全局變量的訪問會自動發送給相應的全局變量。個人不建議使用這種便捷方式。如果所有全局變量都被顯式地聲明爲global,閱讀代碼的人就會看得更清楚。以後,則還可能有機會將全局變量的使用限制在函數內部,僅限極少數情況下才會用到。

動手題:全局變量和局部變量 假定x = 5,在運行以下的funct_1()之後,x的值會是什麼?運行funct_2()之後呢?

def funct_1(): x = 3 def funct_2(): global x x = 2

9.5 將函數賦給變量

與其他Python對象一樣,函數也可以被賦值,如下所示:

>>> def f_to_kelvin(degrees_f):     ⇽---  定義f_to_kelvin函數
...     return 273.15 + (degrees_f - 32) * 5 / 9
...
>>> def c_to_kelvin(degrees_c):     ⇽---  定義c_to_kelvin函數
...     return 273.15 + degrees_c
...
>>> abs_temperature = f_to_kelvin     ⇽---  將f_to_kelvin函數賦給變量
>>> abs_temperature(32)
273.15
>>> abs_temperature = c_to_kelvin     ⇽---  將c_to_kelvin函數賦給變量
>>> abs_temperature(0)
273.15

函數可以被放入列表、元組或字典中:

>>> t = {'FtoK': f_to_kelvin, 'CtoK': c_to_kelvin}     ⇽---  ❶
>>> t['FtoK'](32)       ⇽---  訪問字典中的f_to_kelvin函數
273.15
>>> t['CtoK'](0)      ⇽---  訪問字典中的c_to_kelvin函數
273.15

引用函數的變量,用起來與函數完全相同❶。最後一個例子演示瞭如何使用字典調用各個函數,只要通過用作字符串鍵的值即可。在需要根據字符串值選擇不同函數的情況下,這種模式就很常用。很多時候,這種用法代替了C和Java等語言中的switch結構。

9.6 lambda表達式

上面那種簡短的函數,還可以用lambda表達式來定義:

lambda parameter1, parameter2, . . .: expression

lambda表達式是匿名的小型函數,可以快速地在行內完成定義。通常小型函數是要被傳給另一個函數的,例如,列表的排序方法用到的鍵函數。這種情況下,通常沒有必要定義一個大型函數,而且在使用的地方以外定義也會顯得很彆扭。上一節中的字典就可以在一處完成全部定義:

>>> t2 = {'FtoK': lambda deg_f: 273.15 + (deg_f - 32) * 5 / 9,
...       'CtoK': lambda deg_c: 273.15 + deg_c}     ⇽---  ❶
>>> t2['FtoK'](32)
273.15

以上示例將lambda表達式定義爲字典值❶。注意,lambda表達式沒有return語句,因爲表達式的值將自動返回。

9.7 生成器函數

生成器(generator)函數是一種特殊的函數,可用於定義自己的迭代器(iterator)。在定義生成器函數時,用關鍵字yield返回每一個迭代值。當沒有可迭代值,或者遇到空的return語句或函數結束時,生成器函數將停止返回值。與普通的函數不同,生成器函數中的局部變量值會保存下來,從本次調用保留至下一次調用:

>>> def four():
...     x = 0     ⇽---  將x的初始值設爲0
...     while x < 4:
...         print("in generator, x =", x)
...         yield x     ⇽---  返回x的當前值
...         x += 1     ⇽---  x遞增1
...
>>> for i in four():
...       print(i)
...
in generator, x = 0
0
in generator, x = 1
1
in generator, x = 2
2
in generator, x = 3
3

注意,以上生成器函數包含一個while循環,限定了生成器執行的次數。根據使用方式的不同,調用無停止條件的生成器函數可能會導致無限循環。

yield與yield from的對比

 

從Python 3.3開始,除yield之外,爲生成器函數新增了關鍵字yield from。從本質上說,yield from使將生成器函數串聯在一起成爲可能。yield from的執行方式與yield相同,但是會將當前生成器委託(delegate)給子生成器。簡單一點的話,可以如下使用:

>>> def subgen(x): ... for i in range(x): ... yield i ... >>> def gen(y): ... yield from subgen(y) ... >>> for q in gen(6): ... print(q) ... 0 1 2 3 4 5

以上示例允許將yield表達式移出主生成器,方便了代碼重構。

還可以對生成器函數使用in,以便檢查某值是否屬於生成器生成的一系列值:

>>> 2 in four() in generator, x = 0 in generator, x = 1 in generator, x = 2 True >>> 5 in four() in generator, x = 0 in generator, x = 1 in generator, x = 2 in generator, x = 3 False

速測題:生成器函數 如果要讓上面代碼中的four()函數適用於任何數字,需要如何修改代碼呢?還需要添加什麼代碼,以便能同時設置起始值呢?

9.8 裝飾器

如上所述,因爲函數是Python的一級對象(first-class),所以能被賦給變量。函數也可以作爲實參傳遞給其他函數,還可作爲其他函數的返回值回傳。

例如,可以編寫一個Python函數,它把其他函數作爲形參,並將這個形參包入另一個執行相關操作的新函數中,然後返回這個新函數。這個新的函數組合可用於替換原來的函數:

>>> def decorate(func):
...     print("in decorate function, decorating", func.__name__)
...     def wrapper_func(*args):
...         print("Executing", func.__name__)
...         return func(*args)
...     return wrapper_func
...   
>>> def myfunction(parameter):
...     print(parameter)
...   
>>> myfunction = decorate(myfunction)
in decorate function, decorating myfunction
>>> myfunction("hello")
Executing myfunction
hello

裝飾器(decorator)就是上述過程的語法糖(syntactic sugar),只增加一行代碼就可以將一個函數包裝到另一個函數中去。效果與上述代碼完全相同,不過最終的代碼則更加清晰易懂。

裝飾器用起來十分簡單,由兩部分組成:先定義用於包裝或“裝飾”其他函數的裝飾器函數;然後立即在被包裝函數的定義前面,加上“@”和裝飾器函數名。這裏的裝飾器函數應該是以一個函數爲形參,返回值也是一個函數,如下所示:

>>> def decorate(func):
...     print("in decorate function, decorating", func.__name__)     ⇽---  ❶
...     def wrapper_func(*args):
...         print("Executing", func.__name__)
...         return func(*args)
...     return wrapper_func     ⇽---  ❷
...   
>>> @decorate     ⇽---  ❸
... def myfunction(parameter):
...     print(parameter)
...   
in decorate function, decorating myfunction           
>>> myfunction("hello")     ⇽---  ❹
Executing myfunction
hello

當定義要包裝的函數時,上面的decorate函數會把該函數的名稱打印出來❶。裝飾器函數最後將會返回包裝後的函數❷。通過使用@decorate,myfunction就被裝飾了起來❸。被包裝的函數將會在裝飾器函數執行完畢後調用❹。

裝飾器可將一個函數封裝到另一個函數中,這樣就可以方便地實現很多目標了。在Django之類的Web框架中,裝飾器用於確保用戶在執行函數之前已經處於登錄狀態了。在圖形庫中,裝飾器可用來向圖形框架中註冊函數。

動手題:裝飾器 請修改上述裝飾器函數的代碼,移除無用的消息,並把被包裝函數的返回值用<html>和</html>包起來,以便myfunction("hello")能返回<html>hello<html>。

研究題9:函數的充分利用 回顧第6章和第7章的研究題,請將代碼重構爲清洗和處理數據的函數。目標應該是將大部分邏輯移入函數中。請自行決定函數和參數的類型,但請牢記每個函數只應完成一項功能,而且不應該產生能影響函數外部環境的副作用。

9.9 小結

  • 在函數內部,可以使用global語句訪問外部變量。
  • 實參的傳遞可以根據位置,也可以根據形參的名稱。
  • 函數形參可以有默認值。
  • 函數可以把多個實參歸入元組,以便能定義實參數量不定的函數。
  • 函數可以把多個實參歸入字典,以便能定義實參數量不定的函數,其中實參按照形參的名稱傳入。
  • 函數是Python的一級對象,也就是說函數可以被賦給變量,可以通過變量來訪問,可以被裝飾。

本文摘自《Python快速入門》

Python必學基礎知識之函數

 

本書是一本Python快速入門書,充分體現了Naomi的簡約教學風格,確保你有一本隨手可翻的Python提要,而且這些重點內容都是Python編程的堅實基礎。更爲重要的是,本書能讓你獲得對Python足夠的理解和背景知識,以便自主而高效地動手實踐。有了本書,在成長爲Python開發人員的道路上,你將知道該做什麼、去哪裏尋找答案、遇到困難時該問什麼問題。

Naomi的書正是體現Python風格的典範:優美勝於醜陋,簡單勝於複雜,注重可讀性。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章