part5-1 Python 函數(遞歸函數、參數傳遞方式、變量作用域、局部函數)


函數的特點:
(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 用於聲明訪問當前函數所在函數內的局部變量。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM