函數的特點:
(1)、使用 def 關鍵字定義,有函數名。使用 lambda 定義的函數除外。
(2)、一個函數通常執行一個特定任務,可多次調用,因此函數是代碼利用的重要手段。
(3)、函數調用時可以傳遞0個或多個參數。
(4)、函數有返回值。
一、 函數基礎
函數是 Python 程序的重要組成單位,一個 Python 程序可以由多個函數組成。
1、 理解函數
在定義函數時,至少需要清楚下面3點:
(1)、函數有幾個需要動態變化的數據,這些數據應該被定義成函數的參數。
(2)、函數需要返回幾個重要的數據,這些數據應該被定義成返回值。
(3)、函數的內部實現過程。
函數的定義要比函數的調用難很多。對於實現復雜的函數,定義本身就很費力,所以有時實現不出來也正常。
2、 定義函數和調用函數
函數定義語法格式如下:
1 def function_name(arg1, arg2, ...): 2 # 函數體(由0條或多條代碼組成)
3 [return [返回值]]
語法格式說明如下:
(1)、函數聲明必須使用 def 關鍵字。
(2)、函數名是一個合法的標識符,不要使用 Python 保留的關鍵字;函數名可由一個或多個有意義的單詞連接而成,單詞之間用下划線分隔,單詞的字母全部用小寫。
(3)、形參列表,用於定義該函數可以接收的參數。形參列表由多個形參名組成,形參之間用英文逗號分隔。在定義函數指定了形參,調用該函數時必須傳入對應的參數值,即誰調用函數,誰負責為形參賦值。
函數被調用時,既可以把調用函數的返回值賦值給指定變量,也可以將函數的返回值傳給另一個函數,作為另一個函數的參數。
函數中 return 語句可以顯式地返回一個值,return 語句返回的值既可是有值的變量,也可是一個表達式。
3、 為函數提供文檔
Python 有內置的 help() 函數可查看其他函數的幫助文檔。在編寫函數時,把一段字符串放在函數聲明之后、函數體之前,這段字符串將被作為函數的一部分,這個文檔就是函數的說明文檔。
函數可通過 help() 函數查看函數的說明文檔,也可通過函數的 __doc__ 屬性訪問函數的說明文檔。示例如下:
1 def max_num(x, y):
2 """
3 獲取兩個數值中較大數的函數
4 max_num(x, y)
5 返回x、y兩個參數之間較大的那個數
6 """
7 return x if x > y else y
8 # 使用 help() 函數和 __doc__ 屬性查看 max_num 的幫助文檔
9 help(max_num)
10 print(max_num.__doc__)
運行這段代碼時,可以看到輸出的幫助文檔信息。
4、 多個返回值
如果函數有多個返回值,可將多個值包裝成列表或字典后返回。也可直接返回多個值,這時 Python 會自動將多個返回值封裝成元組。
1 # 定義函數
2 def foo(x, y) 3 return x + y, x - y 4 # 調用函數
5 a1 = foo(10, 5) # 函數返回的是一個元組,變量a1 也就是一個元組
也可使用 Python 提供的序列解包功能,直接用多個變量接收函數返回的多個值,例如:
a2, a3 = foo(10, 5)
5、 遞歸函數
在函數體內調用它自身,被稱為函數遞歸。函數遞歸包含一種隱式的循環,它會重復執行某段代碼,這種重復執行無須循環控制。
現假設有一個數列,f(0)=1, f(1)=4, f(n+2)=2*f(n+1)+f(n),n是大於0的整數,求f(10)的值。這道題用遞歸函數計算,代碼如下:
1 def f(n): 2 if n == 0: 3 return 1
4 elif n == 1: 5 return 4
6 else: 7 # 在函數體內調用其自身,就是遞歸函數
8 return 2 * f(n - 1) + f(n - 2) 9
10 print(f(10)) # 輸出:10497
在這段代碼中,f() 函數體中再次調用了 f() 函數,就是遞歸函數。調用形式是:
return 2 * f(n - 1) + f(n - 2)
對於 f(10)等於 2*f(9)+f(8),其中f(9)又等於2*f(8)+f(7)······,以此類推,最終計算到 f(2)等於2*f(1)+f(0),即f(2)是可計算的,這樣遞歸的隱式循環就有結束的時候,然后一路反算回去最后得到 f(10) 的值。
在上面這個遞歸函數中,必須在某個時刻函數的返回值是確定的,即不再調用它自身;否則,這種遞歸就變成了無窮遞歸,類似於死循環。所以,在定義遞歸時的一條重要規定是:遞歸一定要向已知的方向進行。
現在將這個數列的已知條件改為這樣,已知:f(20)=1, f(21)=4, f(n+2)=2*f(n+1)+f(n),其中n是大於0的整數,求f(10)的值。此時f()函數體就應該改為如下形式:
1 def f(n): 2 if n == 20: 3 return 1
4 elif n == 21: 5 return 4
6 else: 7 return f(n + 2) - 2 * f(n + 1) 8 print(f(10)) # 輸出:-3771
在這次的 f() 函數中,要計算 f(10) 的值時,f(10)等於f(12)-2*f(11),而 f(11)等於f(13)-2*f(12)······,以此類推,直到 f(19)等於f(21)-2*f(20),此時得到 f(19)的值,然后依次反算到f(10)的值。
遞歸在編程中非常有用,例如程序要遍歷某個路徑下的所有文件,但這個路徑下的文件夾的深度是未知的,此時就可用遞歸來實現這個需求。
總結:在一個函數的函數體中調用自身,就是遞歸函數。遞歸一定要向已知的方向進行。
二、 函數的參數
1、關鍵字(keyword)參數
按照形參位置傳入的參數稱為位置參數。如果根據位置參數的方式傳入參數值,則必須嚴格按照定義函數時指定的順序來傳入參數值;也
可根據參數名來傳入參數值,此時無須遵守定義形參的順序,這種方式就是關鍵字(keyword)參數。示例如下:
1 # 定義一個計算周長的函數 girth,接收兩個形參
2 def girth(length, width): 3 print("length:", length) 4 print("width:", width) 5 return 2 * (length + width) 6 # 傳統調用函數方式,根據位置傳入參數值
7 print(girth(3, 5.5)) 8 # 根據關鍵字參數傳入參數值,使用關鍵字時,參數位置可以交換
9 print(girth(width=5.5, length=3)) 10 # 部分用關鍵字,部分用位置參數,位置參數必須在關鍵字參數前面
11 print(girth(3, width=5.5))
在使用關鍵字參數調用函數時要注意,在調用函數時同時使用位置參數和關鍵字參數時,位置參數必須位於關鍵字參數的前面。也就是
關鍵字參數后面只能是關鍵字參數。
2、 參數默認值
在定義函數時,可以為一個或多個形參指定默認值,這樣在調用函數時可以省略為該形參傳入參數值,直接使用該形參的默認值。示例如下:
1 # 為參數指定默認值
2 def say_hi(name='michael', message="歡迎學習使用Python!"): 3 print("hello,", name) 4 print("消息是:", message, sep="") 5 # 第一次調用:使用默認參數調用函數
6 say_hi() 7 # 第二次調用:傳入1個位置參數時,默認傳給第一個參數
8 say_hi('jack') 9 # 第三次調用:傳入2個位置參數
10 say_hi('stark', '歡迎使用 C 語言!') 11 # 使用關鍵字參數指明要傳給哪個參數
12 say_hi(message="歡迎使用 Linux 系統!") 13
14 輸出如下所示: 15 hello, michael 16 消息是:歡迎學習使用Python! 17 hello, jack 18 消息是:歡迎學習使用Python! 19 hello, stark 20 消息是:歡迎使用 C 語言! 21 hello, michael 22 消息是:歡迎使用 Linux 系統!
從這個程序可知,當只傳入一個位置參數時,由於該參數位於第一位,系統會將該參數值傳給 name 參數。
在Python 中,關鍵字參數必須位於位置參數后面,所以下面把關鍵字參數放在前面是錯誤的做法:
say_hi(name='stark', '歡迎使用 C 語言!')
這樣調用函數時,報 “positional argument follows keyword argument” 錯誤。
在調用函數時,也不能簡單的交換兩個參數的位置,下面對函數的調用方式同樣是錯誤的:
say_hi('歡迎使用 C 語言!', name='stark')
因為第一個字符串沒有指定關鍵字參數,因此使用位置參數為 name 參數傳入參數值,第二個參數使用關鍵字參數的形式再次為 name 參數傳入參數值,這樣造成兩個參數值都會傳給 name 參數,程序為 name 參數傳入了多個參數值。因此錯誤提示:say_hi() got multiple values for argument 'name'。
Python 要求在調用函數時關鍵字參數必須位於位置參數后面,因此在定義函數時指定默認值的參數(關鍵字參數)必須在沒有默認值的參數之后。例如:
1 def foo(a, b=10): pass # 有默認值的參數在沒有默認值的參數后面
2 下面這些調用函數的方法都是正確的: 3 foo(5) 4 foo(a=5, b=4) 5 foo(5, 4) 6 foo(a='python')
3、 參數收集(個數可變的參數)
在定義函數時,形參前面加一個星號(*)表示該參數可接收多個參數值,多個參數值被當成元組傳入。示例如下:
1 def my_test(a, *args): 2 """測試支持參數收集的函數"""
3 print(args) 4 # args 參數被當成元組處理
5 for s in args: 6 print(s) 7 print(a) 8 my_test(123, 'python', 'linux', 'C') 9
10 輸出如下所示: 11 ('python', 'linux', 'C') 12 python 13 linux 14 C 15 123
從輸出可知,在調用 my_test() 函數時,args 參數可以傳入多個字符串作為參數值,參數收集的本質就是一個元組,將傳給 args 的多個值收集成一個元組。
對於個數可變的形參可以處於形參列表的任意位置,但是函數最多只能有一個“普通”參數收集的形參。例如下面這樣:
def my_test(*args, a): pass
在定義函數時,把個數可變的形參放在前面。那么在調用該函數時,如果要給后面的參數傳入參數值,就必須使用關鍵字參數;否則,程序會把所傳入的多個值都當成是傳給 args 參數的。
另外,Python 還可以將關鍵字參數收集成字典,這時在定義函數時,需要在參數前面加兩個星號(**)。一個函數可同時包含一個支持“普通”參數收集的參數和一個支持關鍵字參數收集的參數。示例如下:
1 def bar(a, b, c=5, *args, **kwargs): pass
2 bar(1, 2, 3, "python", "linux", name=stark, age=30)
在調用 bar() 函數時,前面的1、2、3會傳給普通參數a、b、c;后面緊跟的兩個參數由 args 收集成元組;最后的兩個關鍵字參數被kwargs 收集成字典。這個 bar() 函數中,c 參數的默認值基本上不能發揮作用。要使 c 參數的默認起作用,可用下面方式調用函數:
bar(1, 2, name=stark, age=30)
4、 逆向參數收集
逆向參數收集指的是在程序中將已有的列表、元組、字典等對象,將其拆分后傳給函數的參數。逆向參數收集需要在傳入的列表、元組等參數之前添加一個星號,在字典參數之前添加兩個星號。示例如下:
1 def bar(s1, s2): 2 print(s1) 3 print(s2) 4 s = 'ab'
5 bar(*s) 6 ss = ("python", "linux") 7 bar(*ss) 8 ss_dict = {"s1": "michael", "s2": 25} 9 bar(**ss_dict)
在這個 bar() 函數中,定義時聲明了兩個形參,在調用函數時,使用一個星號拆分序列(字符串、列表、元組)時,拆分出來的個數也應和形參的個數一樣多。使用兩個星號傳遞字典參數時,則要求字典的鍵與函數的形參名保持一致,並且字典的鍵值對與函數的形參個數也要一致。
即使支持收集的參數,如果要將一個字符串或元組傳給該參數,同樣也需要使用逆向收集。示例如下:
1 def bar(s1, *s2): 2 print(s1) 3 print(s2) 4 s = 'abcd'
5 bar("py", *s) 6 ss = ("python", "linux") 7 bar("java", *ss)
這次的函數調用中,可以使用一個星號拆分序列,序列的元素個數可以有多個。但是不能使用兩個星號拆分字典后向其傳參數。實際上,在調用函數時,只傳遞一個拆分后的序列參數也是可以的,例如:
bar(*s)
這時程序會將 s 進行逆向收集,其中字符串的第一個元素傳給 s1 參數,后面的元素傳給 s2 參數。如果不使用逆向收集(不在序列參數前使用星號),則會將整個序列作為一個參數,而不是將序列的元素作為多個參數。例如:
bar(s)
這次調用沒有使用逆向收集,因此 s 整體作為參數值傳給 s1 參數。
字典也支持逆向收集,字典以關鍵字參數的形式傳入。這要求字典的鍵與函數的形參名一致,並且字典的鍵值對數量與函數的形參數量一樣多。示例在前面已提到。
5、 函數的參數傳遞機制
Python 中函數的參數傳遞機制都是“值傳遞”,就是將實際參數值的副本(復制品)傳入函數,而參數本身不會受到任何影響。示例如下:
1 def swap(a, b): 2 a, b = b, a 3 print("在swap函數里,a的值是%s; b 的值是%s" % (a, b)) 4 a = 10
5 b = 20
6 swap(a, b) 7 print("變換結束后,變量 a 的值是%s;變量 b 的值是%s。" % (a, b)) 8
9 運行代碼,輸出如下: 10 在swap函數里,a的值是20; b 的值是10 11 變換結束后,變量 a 的值是10;變量 b 的值是20。
在這段代碼中,swap() 函數中交換了變量a、b 的值,在函數內輸出的是交換后的值,在函數外部仍然是未交換時的值。由此可知,程序中實際定義的變量 a 和 b,並不是 swap() 函數里的 a 和 b。在 swap() 函數里的 a 和 b 是主程序中變量 a 和 b 的復制品。
在主程序中調用 swap() 函數時,系統分別為主程序和 swap() 函數分配兩塊棧區,用於保存它們的局部變量。將主程序中的 a、b 變量作為參數值傳入 swap() 函數,實際上是在 swap() 函數棧區中重新產生了兩個變量 a、b,並將主程序棧區的 a、b 變量的值分別賦值給 swap() 函數棧區中的 a、b 參數。此時系統存在兩個 a 變量、兩個 b 變量,只是存在於不同的棧區中。
參數值傳遞實質:當系統開始執行函數時,系統對形參執行初始化,就是把實參變量的值賦給函數的形參變量,在函數中操作的並不是實際的實參變量。
要注意的是,如果參數本身是可變對象(比如列表、字典等),此時同樣也是采用的值傳遞方式,但是在函數中變量存儲的是可變對象的內存地址,因此在函數中對可變對象進行修改時,修改結果會反應到原始的可變對象上。
關於參數傳遞機制的總結:
(1)、不管什么類型的參數,在 Python 函數中對參數直接使用 “=” 符號賦值是沒用的,直接使用 “=” 符號賦值並不能改變參數。
(2)、如果要讓函數修改某些數據,則可以通過把這些數據包裝成列表、字典等可變對象,然后把列表、字典等可變對象作為參數傳入函數,在函數中通過列表、字典的方法修改它們。這樣才能改變這些數據。
6、 變量作用域
程序中定義的變量有作用范圍,叫做作用域。變量分為兩種:
(1)、局部變量:在函數中定義的變量,包括參數,都被稱為局部變量。
(2)、全局變量:在函數外面、全局范圍內定義的變量,被稱為全局變量。
函數在執行時,系統為函數分配一塊臨時內存空間,所有局部變量被保存在這塊臨時空間內。函數執行完成后,這塊臨時內存空間就被釋放,同時局部變量就失效。所以離開函數后,就不能訪問局部變量。
全局變量可以在所有函數內被訪問到。不管是局部變量還是全局變量,變量和它們的值就像一個“看不見”的字典,變量名是 key,變量值是字典的 value。Python 提供下面三個工具函數來獲取指定范圍內的“變量字典”:
(1)、globals():返回全局范圍內所有變量組成的“變量字典”。
(2)、locals():返回當前局部范圍內所有變量組成的“變量字典”。
(3)、vars(object):獲取在指定對象范圍內所有變量組成的“變量字典”。不傳入 object 參數,vars() 和 locals() 作用完全相同。
globals()和locals()的區別與聯系:
(1)、locals() 總是獲取當前局部范圍內所有變量組成的“變量字典”。因此,在全局范圍內(在函數之外)調用locals() 函數,同樣會獲取全局范圍內所有變量組成的“變量字典”;而globlas() 無論在哪里執行,總是獲取全局范圍內所有變量組成的“變量字典”。
(2)、通常使用 locals()和globals()獲取的“變量字典”只應該被訪問,不應該被修改。實際上不管使用globlas()還是locals()獲取的全局范圍內的“變量字典“,都可以被修改,則這種修改會真正修改全局變量本身;但通過locals()獲取的局部范圍內的”變量字典“,即使對它修改也不會影響局部變量。
下面用代碼理解 locals() 和 globlas() 函數的使用:
1 def test(): 2 name = 'michael'
3 print(name) # 輸出:michael
4 # 訪問函數局部范圍內的”變量字典“
5 print(locals()) # 輸出:{'name': 'michael'}
6 # 通過函數局部范圍內的”變量數組“訪問 name 變量
7 print(locals()['name']) # 輸出:michael
8 # 通過 locals() 函數修改局部變量的值,即使修改了也不會對局部變量有什么影響
9 locals()['name'] = 'stark'
10 # 再次訪問 name 變量的值
11 print("modify: ", locals()['name']) # 輸出:modify: michael
12 # 通過 globlas() 函數修改全局變量 x 的值
13 globals()['x'] = 10
14 x = 1
15 y = 2
16 # 在全局范圍內調用 globals() 函數和 locals() 函數,訪問的是全局變量的”變量字典",兩個函數輸出的結果一樣
17 print(globals()) # 輸出:{..., 'x': 1, 'y': 2}
18 print(locals()) # 輸出:{..., 'x': 1, 'y': 2}
19 # 在全局范圍內直接使用 globlas 和 locals 函數訪問全局變量
20 print(globals()['x']) # 輸出:1
21 print(locals()['x']) # 輸出:1
22 # 在全局范圍地內使用 globlas 和 locals 函數修改全局變量的值
23 globals()['x'] = 30
24 locals()['y'] = 20
25 # 從輸出可知,在全局范圍內,使用 globlas 和 locals 函數修改全局變量的值都會修改成功
26 print("modify: ", globals()['x']) # modify: 30
27 print("modify: ", locals()['y']) # modify: 20
28
29 test() # 在函數內部使用 globals 函數修改全局變量的值也會修改成功
30 print("modify two: ", globals()['x']) # modify two: 10
從函數輸出可知,locals() 函數用於訪問特定范圍內的所有變量組成的”變量字典“,但是不能修改特定范圍內”變量字典“中變量的值。在全局范圍不管使用 locals() 函數還是 globals() 函數都可以修改全局范圍內”變量字典“中變量的值。globals() 函數在特定范圍內也可以修改全局范圍內”變量字典“中變量的值。
全局變量可以在所有函數內被訪問,但是在函數內定義了與全局變量同名的變量時,此時全局變量會被局部變量遮蔽。示例如下:
1 name = 'michael'
2 def test():
3 # 直接訪問 name 的全局變量
4 print(name)
5 name = 'stark'
6 test()
運行這段代碼,此時程序會報錯,錯誤信息是:UnboundLocalError: local variable 'name' referenced before assignment。錯誤信息提示在函數內訪問的 name 變量還未定義,這是由於在 test() 函數中增加了 ”name='start'“ 這行代碼造成的。
Python 語法規定:在函數內部對不存在的變量賦值時,默認就是重新定義新的局部變量。因此這行 ”name='start'“ 相當於重新定義了 name 局部變量,這樣 name 全局變量就被遮蔽了,所以代碼會報錯。為了讓程序不報錯,可用兩種方式修改上面代碼。
第一種方式:訪問被遮蔽的全局變量
在 test() 函數中希望 print 語句仍然能訪問 name 全局變量,並且要在 print 語句之后重新定義 name 局部變量,也就是在函數中可能訪問被遮蔽的全局變量,此時可通過 globals() 函數來實現。代碼修改如下:
1 name = 'michael'
2 def test():
3 # 直接訪問 name 的全局變量
4 print(globals()['name']) # 輸出:michael
5 name = 'stark' # 定義局部變量
6 test()
7 print(name) # 輸出:michael
第二種方式:在函數中聲明全局變量
為避免在函數中對全局變量賦值(不是重新定義局部變量),可使用 globals 語句來聲明全局變量。代碼可修改為如下形式:
1 name = 'michael'
2 def test():
3 # 先聲明 name 是全局變量,后面的賦值語句不會重新定義局部變量,而是直接修改全局變量
4 global name
5 # 直接訪問 name 全局變量
6 print(name) # 輸出:michael
7 name = 'stark' # 修改全局變量
8 test()
9 print(name) # 輸出:stark
在test() 函數中的”global name“ 聲明 name 為全局變量,后面對 name 賦值的語句只是對全局變量賦值,不是重新定義局部變量。
三、 局部函數
Python 支持在函數體內定義函數,這種放在函數體內定義的函數稱為局部函數。默認情況下,局部函數對外部是隱藏的,局部函數只能在其封閉(enclosing)函數內有效,其封閉函數也可以返回局部函數,以便程序在其他作用域中使用局部函數。局部函數示例:
1 def foo(type, nn): 2 """定義一個函數,該函數包含局部函數"""
3 def square(n): 4 """定義一個計算平方的局部函數"""
5 return n * n 6 def cube(n): 7 """定義一個計算立方的局部函數"""
8 return n * n * n 9 def factorial(n): 10 """定義一個計算階乘的局部函數"""
11 result = 1
12 for i in range(2, n + 1): 13 result *= i 14 return result 15 # 調用局部函數
16 if type == 'square': 17 return square(nn) 18 elif type == 'cube': 19 return cube(nn) 20 else: 21 return factorial(nn) 22 print(foo('square', 5)) # 輸出:25
23 print(foo('cube', 3)) # 輸出:27
24 print(foo('', 3)) # 輸出:6
這里的 foo() 函數體內定義了3個局部函數,foo() 函數根據參數選擇調用不同的局部函數。如果封閉函數(foo())沒有返回局部函數,那么局部函數只能在封閉函數內部調用。例如上面的 foo() 函數。
另一種情況是,封閉函數將局部函數返回,且程序使用變量保存了封閉函數的返回值,那么這些局部函數的作用域就會被擴大,程序可通過該變量自由的調用它們,就像它們是全局函數一樣。
局部函數內的變量也會遮蔽它所在函數內的局部變量。示例如下:
1 def foo(): 2 # 局部變量 name
3 name = 'michael'
4 def bar(): 5 # 訪問 bar 函數所在 foo 函數內的 name 局部變量
6 print(name) # michael
7 name = 'stark'
8 bar() 9 foo()
運行這段代碼,出現錯誤提示“UnboundLocalError: local variable 'name' referenced before assignment”。這錯誤是由於局部變量遮蔽局部變量導致的,在 bar() 函數中定義的 name 局部變量遮蔽了它所在 foo() 函數內的 name 局部變量,因此導致程序中 print 語句代碼報錯。
為了使 bar() 函數內的“name = 'stark'” 賦值語句不是定義新的局部變量,只是訪問它所在 foo() 函數內的 name 局部變量,可使用Python 提供的 nonlocal 關鍵字,通過 nonlocal 語句即可聲明訪問賦值語句只是訪問該函數所在函數內的局部變量。示例如下:
1 def foo(): 2 # 局部變量 name
3 name = 'michael'
4 def bar(): 5 # 訪問 bar 函數所在 foo 函數內的 name 局部變量
6 nonlocal name 7 print(name) # michael
8 name = 'stark'
9 bar() 10 foo()
在 foo() 函數內增加 “nonlocal name” 語句后,在 bar() 函數中的 “name = 'stark'” 就不再是定義新的局部變量,而是訪問它所在函數(foo())內的 name 局部變量。
nonlocal 的功能和 global 功能大致相似,區別是 global 用於聲明全局變量,而 nonlocal 用於聲明訪問當前函數所在函數內的局部變量。