本節內容
- 函式的介紹
- 函式的建立
- 函式引數及返回值
- LEGB作用域
- 特殊函式
- 函數語言程式設計
1.函式的介紹
為什麼要有函式?因為在平時寫程式碼時,如果沒有函式的話,那麼將會出現很多重複的程式碼,這樣程式碼重用率就比較低。。。並且這樣的程式碼維護起來也是很有難度的,為了解決這些問題,就出現了函式,用來將一些經常出現的程式碼進行封裝,這樣就可以在任何需要呼叫這段程式碼的地方呼叫這個函式就行了。
函式的定義:函式是指將一組語句的集合通過一個名字(函式名)封裝起來,要想執行這個函式,只需呼叫其函式名即可
特性:
- 程式碼重用
- 保持一致性
- 可擴充套件性
2.函式的建立
在python中函式定義的格式如下:
1 2 3 |
def 函式名(形參): 函式體內部程式碼塊 |
函式的呼叫使用 函式名(實參) 就可以呼叫函式了。
函式名的命名規則和變數的命名規則一樣:
- 函式名必須以下劃線或字母開頭,可以包含任意字母、數字或下劃線的組合。不能使用任何的標點符號;
- 函式名是區分大小寫的。
- 函式名不能是保留字。
形參和實參的區別:
函式在定義的時候,函式名後面的括號中可以新增引數,這些引數就叫做形參,形參:顧名思義就是形式引數,只是一個代號。
實參是在呼叫函式的時候函式名後面的括號中的引數,形參和實參需要一一對應起來,否則呼叫函式會報錯。
3.函式引數及返回值
前面提到函式的形參和實參要一一對應,那麼引數對應有如下幾種:
- 必須引數
- 關鍵字引數
- 預設引數
- 不定長引數 *args
- 不定長引數 **kwargs
1.必須引數:
必須引數必須以對應的關係一個一個傳遞進入函式,函式呼叫時傳遞的實參必須和函式定義時的形參一一對應,不能多也不能少,順序也得一致。
舉個例子:
1 2 3 |
1 def f(name,age): 2 print(name,age) 3 f("小明",18) |
2.關鍵字引數
關鍵字引數是實參裡面的概念,在呼叫函式的時候宣告某個引數是屬於某個關鍵字的。使用關鍵字引數允許函式呼叫時引數的順序與宣告時不一致,因為 Python 直譯器能夠用引數名匹配引數值。
舉個例子:
1 2 3 |
1 def f(name,age): 2 print(name,age) 3 f(name="小明",18) |
3.預設引數
預設引數是在函式宣告的時候,可以給某個引數指定預設值,這樣的引數叫做預設值引數。如果在呼叫函式的時候,預設引數沒有接收到對應的實參,那麼就會將預設值賦值給這個引數。
舉個例子:
1 2 3 |
1 def f(name,age,sex="male"): 2 print(name,age,sex) 3 f(name="小明",18) |
這樣,就會把預設引數male賦值給sex了。
4.不定長引數 *args
在python裡面,函式在宣告的時候,引數中可以使用(*變數名)的方式來接受不確定長度的引數,但是在python裡面大家約定俗成使用*args接受不定長引數,這樣在呼叫函式的時候傳遞的引數就可以是不定長度的了。args接受了不定長引數之後,將這些引數放到一個tuple裡面,可以通過訪問args來獲取這些不定長引數。
舉個例子:
1 2 3 |
1 def f(*args): 2 print(args) 3 f("小明",18,"male") |
列印出來的是一個tuple,裡面存放了(“小明”,18,”male”)這三個元素。
不定長引數 **kwargs
但是上面的args只能接收未命名的引數,那假如有類似於關鍵字引數的不定長引數該怎麼辦呢?python裡面使用(**變數名)來接收不定長的命名變數引數。同樣,python裡面也約定俗成使用**kwargs接收不定長命名引數。kwargs接收了不定長引數之後,將這些引數放到一個字典裡面,可以通過key獲取到相應的引數值。
舉個例子:
1 2 3 |
1 def f(**kwargs): 2 print(kwargs) 3 f(name="小明",age=18,sex="male") |
介紹完了這些引數之後,接下來要介紹的是關於這些引數混合使用的情況:
假如一個函式使用了上面所有種類的引數,那該怎麼辦?為了不產生歧義,python裡面規定了假如有多種引數混合的情況下,遵循如下的順序使用規則:
1 2 |
1 def f(必須引數,預設引數,*args,**kwargs): 2 pass |
如果同時存在args和kwargs的話,args在左邊
預設引數在必須引數的右邊,在*args的左邊
關鍵字引數的位置不固定(ps:關鍵字引數也不在函式定義的時候確定)
那麼,假如有一個列表想要傳遞進入一個不定長的未命名引數的函式中去,可以在該列表前面加上*實現,同理如果想傳遞一個字典進入不定長命名引數的函式中去,可以在該字典前面加上**
舉個例子:
1 2 3 4 5 6 |
1 def f(*args,**kwargs): 2 print(args) 3 for i in kwargs: 4 print("%s:%s"%(i,kwargs[i])) 5 6 f(*[1,2,3],**{"a":1,"b":2}) |
函式的返回值
要想獲取函式的執行結果,就可以用return語句把結果返回
注意:
函式在執行過程中只要遇到return語句,就會停止執行並返回結果,也可以理解為 return 語句代表著函式的結束 如果未在函式中指定return,那這個函式的返回值為None
return多個物件,直譯器會把這多個物件組裝成一個元組作為一個一個整體結果輸出。
4.LEGB作用域
python中的作用域分4種情況:
L:local,區域性作用域,即函式中定義的變數;
E:enclosing,巢狀的父級函式的區域性作用域,即包含此函式的上級函式的區域性作用域,但不是全域性的;
G:globa,全域性變數,就是模組級別定義的變數;
B:built-in,系統固定模組裡面的變數,比如int, bytearray等。 搜尋變數的優先順序順序依次是:作用域區域性>外層作用域>當前模組中的全域性>python內建作用域,也就是LEGB。
local和enclosing是相對的,enclosing變數相對上層來說也是local。
在Python中,只有模組(module),類(class)以及函式(def、lambda)才會引入新的作用域,其它的程式碼塊(如if、try、for等)不會引入新的作用域。
變數的修改(錯誤修改,面試題裡經常出):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
1 x=6 2 def f2(): 3 print(x) 4 x=5 5 f2() 6 7 # 錯誤的原因在於print(x)時,直譯器會在區域性作用域找,會找到x=5(函式已經載入到記憶體),但x使用在宣告前了,所以報錯: 8 # local variable 'x' referenced before assignment.如何證明找到了x=5呢?簡單:註釋掉x=5,x=6 9 # 報錯為:name 'x' is not defined 10 #同理 11 x=6 12 def f2(): 13 x+=1 #local variable 'x' referenced before assignment. 14 f2() |
global關鍵字
當內部作用域想修改外部作用域的變數時,就要用到global和nonlocal關鍵字了,當修改的變數是在全域性作用域(global作用域)上的,就要使用global先宣告一下,程式碼如下:
1 2 3 4 5 6 7 |
1 count = 10 2 def outer(): 3 global count 4 print(count) 5 count = 100 6 print(count) 7 outer() |
nonlocal關鍵字
global關鍵字宣告的變數必須在全域性作用域上,不能巢狀作用域上,當要修改巢狀作用域(enclosing作用域,外層非全域性作用域)中的變數怎麼辦呢,這時就需要nonlocal關鍵字了
1 2 3 4 5 6 7 8 9 |
1 def outer(): 2 count = 10 3 def inner(): 4 nonlocal count 5 count = 20 6 print(count) 7 inner() 8 print(count) 9 outer() |
小結
- 變數查詢順序:LEGB,作用域區域性>外層作用域>當前模組中的全域性>python內建作用域;
- 只有模組、類、及函式才能引入新作用域;
- 對於一個變數,內部作用域先宣告就會覆蓋外部變數,不宣告直接使用,就會使用外部作用域的變數;
- 內部作用域要修改外部作用域變數的值時,全域性變數要使用global關鍵字,巢狀作用域變數要使用nonlocal關鍵字。nonlocal是python3新增的關鍵字,有了這個 關鍵字,就能完美的實現閉包了。
5.特殊函式
遞迴函式定義:遞迴函式就是在函式內部呼叫自己
有時候解決某些問題的時候,邏輯比較複雜,這時候可以考慮使用遞迴,因為使用遞迴函式的話,邏輯比較清晰,可以解決一些比較複雜的問題。但是遞迴函式存在一個問題就是假如遞迴呼叫自己的次數比較多的話,將會使得計算速度變得很慢,而且在python中預設的遞迴呼叫深度是1000層,超過這個層數將會導致“爆棧”。。。所以,在可以不用遞迴的時候建議儘量不要使用遞迴。
舉個例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
1 def factorial(n): # 使用迴圈實現求和 2 Sum=1 3 for i in range(2,n+1): 4 Sum*=i 5 return Sum 6 print(factorial(7)) 7 8 def recursive_factorial(n): # 使用遞迴實現求和 9 return (2 if n==2 else n*recursive_factorial(n-1)) 10 11 print(recursive_factorial(7)) 12 13 def feibo(n): # 使用遞迴實現菲波那切數列 14 if n==0 or n==1:return n 15 else:return feibo(n-1)+feibo(n-2) 16 print(feibo(8)) 17 18 def feibo2(n): # 使用迴圈實現菲波那切數列 19 before,after=0,1 20 for i in range(n): 21 before,after=after,before+after 22 return before 23 print(feibo2(300)) |
遞迴函式的優點:定義簡單,邏輯清晰。理論上,所有的遞迴函式都可以寫成迴圈的方式,但迴圈的邏輯不如遞迴清晰。
遞迴特性:
- 必須有一個明確的結束條件
- 每次進入更深一層遞迴時,問題規模相比上次遞迴都應有所減少
- 遞迴效率不高,遞迴層次過多會導致棧溢位(在計算機中,函式呼叫是通過棧(stack)這種資料結構實現的,每當進入一個函式呼叫,棧就會加一層棧幀,每當函式返 回,棧就會減一層棧幀。由於棧的大小不是無限的,所以,遞迴呼叫的次數過多,會導致棧溢位。)
6.函數語言程式設計
關於函數語言程式設計,我理解的也不是很深,但是python中有4個比較重要的內建函式,組合起來使用有時候能大大提高程式設計效率。
1 filter(function, sequence)
1 2 3 4 5 |
1 str = ['a', 'b','c', 'd'] 2 def fun1(s): 3 if s != 'a': 4 return s 5 ret = filter(fun1, str) |
print(list(ret))# ret是一個迭代器物件
對sequence中的item依次執行function(item),將執行結果為True的item做成一個filter object的迭代器返回。可以看作是過濾函式。
2 map(function, sequence)
1 2 3 4 5 6 |
1 str = [1, 2,'a', 'b'] 2 def fun2(s): 3 return s + "alvin" 4 ret = map(fun2, str) 5 print(ret) # map object的迭代器 6 print(list(ret))# ['aalvin', 'balvin', 'calvin', 'dalvin'] |
對sequence中的item依次執行function(item),將執行結果組成一個map object迭代器返回. map也支援多個sequence,這就要求function也支援相應數量的引數輸入:
1 2 3 |
1 def add(x,y): 2 return x+y 3 print (list(map(add, range(10), range(10))))##[0, 2, 4, 6, 8, 10, 12, 14, 16, 18] |
3 reduce(function, sequence, starting_value)
1 2 3 4 5 6 |
1 from functools import reduce 2 def add1(x,y): 3 return x + y 4 5 print (reduce(add1, range(1, 101)))## 4950 (注:1+2+...+99) 6 print (reduce(add1, range(1, 101), 20))## 4970 (注:1+2+...+99+20) |
對sequence中的item順序迭代呼叫function,如果有starting_value,還可以作為初始值呼叫.
4 lambda
普通函式與匿名函式的對比:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
1 #普通函式 2 def add(a,b): 3 return a + b 4 5 print add(2,3) 6 7 8 #匿名函式 9 add = lambda a,b : a + b 10 print add(2,3) 11 12 13 #========輸出=========== 14 5 15 5 |
匿名函式的命名規則,用lamdba 關鍵字標識,冒號(:)左側表示函式接收的引數(a,b) ,冒號(:)右側表示函式的返回值(a+b)。
因為lamdba在建立時不需要命名,所以,叫匿名函式