Python函數初識二
一、變量的作用域LEGB
1.1、變量的作用域
在Python中,程序的變量並不是在哪個位置都可以訪問的,訪問權限決定於這個變量是在哪裏賦值的。變量的作用域決定了在哪一部分程序可以訪問哪個特定的變量名稱。
在Python程序中創建、改變、查找變量名時,都是在一個保存變量名的空間中進行,我們稱之為命名空間,也被稱之為作用域。python的作用域是靜態的,在源代碼中變量名被賦值的位置決定了該變量能被訪問的範圍。即Python變量的作用域由變量所在源代碼中的位置決定。
1.2、變量作用域的產生
在Python中並不是所有的語句塊中都會產生作用域。只有當變量在Module(模塊)、Class(類)、def(函數)中定義的時候,才會有作用域的概念。
舉個栗子:
def greet_people():
name = 'jack'
print("Hello, "+name) # 在函數內打印變量
print(name) # 在函數外打印變量 ===》 直接出錯
greet_people()
上述代碼的運行結果為:
C:\Python37\python3.exe D:/pythoncode/Exercise/Exer8/Exer8-13.py Traceback (most recent call last): File "D:/pythoncode/Exercise/Exer8/Exer8-13.py", line 5, in <module> print(name) NameError: name 'name' is not defined Process finished with exit code 1
分析:在作用域中定義的變量,一般只在作用域中有效,上面是在greet_people() 函數中定義的變量 name ,在函數外調用就會出錯,因為函數外就不是變量name的作用域了。
註意:在if-elif-else、for-else、while、try-except\try-finally等關鍵字的語句塊中並不會產成作用域 ,只要是定義的變量,都可以調用。
1.3、變量作用域的分類
Python中變量的作用域可分為以下四種:
- L (Local) 局部作用域
- E (Enclosing) 閉包函數外的函數中
- G (Global) 全局作用域
- B (Built-in) 內建作用域
註意:上述變量作用域是按照 L –> E –> G –> B
舉個栗子:
x = int(2.9) # 內建作用域
g_count = 0 # 全局作用域
def outer():
o_count = 1 # 閉包函數外的函數中,enclosing
def inner():
i_count = 2 # 局部作用域
詳細介紹上面的四種作用域:
L(local)局部作用域
局部變量:包含在def關鍵字定義的語句塊中,即在函數中定義的變量。每當函數被調用時都會創建一個新的局部作用域。在函數內部的變量聲明,除非特別的聲明為全局變量,否則均默認為局部變量。有些情況需要在函數內部定義全局變量,這時可以使用global關鍵字來聲明變量的作用域為全局變量,Python中也有遞歸,即自己調用自己,每次調用都會創建一個新的局部命名空間。
註意:如果需要在函數內部對全局變量賦值,需要在函數內部通過global語句聲明該變量為全局變量。
E(enclosing)嵌套作用域
E也包含在def關鍵字中,E和L是相對的,E相對於更上層的函數而言也是L。與L的區別在於,對一個函數而言,L是定義在此函數內部的局部作用域,而E是定義在此函數的上一層父級函數的局部作用域。主要是為了實現Python的閉包,而增加的實現。
G(global)全局作用域
即在模塊層次中定義的變量,每一個模塊都是一個全局作用域。也就是說,在模塊文件頂層聲明的變量具有全局作用域,從外部開來,模塊的全局變量就是一個模塊對象的屬性。
註意:全局作用域的作用範圍僅限於單個模塊文件內
B(built-in)內置作用域
系統內固定模塊裏定義的變量,比如int, bytearray等
搜索變量的優先級順序依次是:作用域局部 > 外層作用域 > 當前模塊中的全局 > python內置作用域,也就是LEGB
舉個栗子:
globalVar = 100 #globalVar是全局變量,作用於全局作用域
def test_scope():
enclosingVar = 200 #enclosingVar是嵌套變量,作用於test_scope函數以及其包含的func函數
def func():
localVar = 300 #localVar局部變量,作用於func函數
print(localVar) #==> localVar=300,局部變量作用域作用於本地函數func
print(globalVar) #====> globalVar=100,說明全局變量作用域群局作用域
print(enclosingVar) #===> enclosingVar=200,說明嵌套變量作用域包含其內部包含的變量
func()
print(localVar) #==> 直接出錯,出了func函數的作用域局部變量就不能引用了
print(globalVar) #====> globalVar=100,說明全局變量作用域群局作用域
print(enclosingVar) #===> enclosingVar=200,嵌套函數在自己頂層函數test_scope的作用域內有效
test_scope()
print(localVar) #==> 直接出錯,出了func函數的作用域局部變量就不能引用了
print(globalVar) #====> globalVar=100,說明全局變量作用域群局作用域
print(enclosingVar) #==> 直接出錯,說明相出整個頂層函數test_scope的作用域,嵌套變量也是無效的, 相當於出了局部變量的函數作用域
print(__name__) #內置變量,直接調用
上面這個例子基本上就是各種變量作用域的測試。
下面再看兩個例子:
variable = 300 # 定義全局變量variable
def test_scopt():
print(variable) ====> 輸出variable=300 直接調用全局變量variable
test_scopt()
print(variable) ====> 輸出variable=300 直接調用全局變量variable
variable = 300 # 定義全局變量variable
def test_scopt():
print(variable) ====> 直接報錯 #錯誤的原因在於print(variable)時,解釋器會在局部作用域找,會找到 variable = 300(函數已經加載到內存),但variable使用在聲明前了,所以報錯;
variable = 200 #定義局部變量
test_scopt()
print(variable) ====> 輸出variable=300 直接調用全局變量variable
上述代碼的運行結果為:
C:\Python37\python3.exe D:/pythoncode/Exercise/Exer8/Exer8-13.py
Traceback (most recent call last):
File "D:/pythoncode/Exercise/Exer8/Exer8-13.py", line 36, in <module>
test_scopt()
File "D:/pythoncode/Exercise/Exer8/Exer8-13.py", line 33, in test_scopt
print(variable)
UnboundLocalError: local variable 'variable' referenced before assignment
Process finished with exit code 1
分析:上述代碼在函數 test_scopt 中打印變量variable時直接程序出錯,因為在執行程序時的預編譯能夠在test_scopt()中找到局部變量variable(對variable進行了賦值)。在局部作用域找到了變量名,所以不會升級到嵌套作用域去尋找。但是在使用print語句將變量variable打印時,局部變量variable並有沒綁定到一個內存對象(沒有定義和初始化,即沒有賦值)。本質上還是Python調用變量時遵循的LEGB法則和Python解析器的編譯原理,決定了這個錯誤的發生。所以,在調用一個變量之前,需要為該變量賦值(綁定一個內存對象)。
1.4、變量作用域的修改(不建議使用)
變量性質不同其作用域也是不同的,當內部作用域想修改外部作用域的變量時,就要用到global和nonlocal關鍵字了,當修改的變量是在全局作用域(global作用域)上的,就要使用global先聲明一下;global語句是一個命名空間的聲明,它告訴Python解釋器打算生成一個或多個全局變量,也就是說,存在於整個模塊內部作用域(命名空間)的變量名。
舉個栗子:
variable = 300 # ====> 定義全局變量
def test_scopt():
global variable # ====> 通過global語句聲明變量variable,使其在函數test_scopt內可以被直接調用
print(variable) ====> 輸出值為:300
variable = 200 # ====> 定義局部變量
print(variable) ====> 輸出值為:200
test_scopt()
print(variable) ====> 輸出值為:200,因為global語句將variable變量變成全局變量了
註意:global語句包含了關鍵字global,其後跟著一個或多個由逗號分開的變量名。當在函數主題被賦值或引用時,所有列出來的變量名將被映射到整個模塊的作用域內。
但是在實際工作中,盡量不要使用global改變局部變量作用域,因為如果代碼量非常多的時候不好調試,不知道哪裏函數作用域範圍出錯,盡量不要改變變量作用域以免導致出錯。
也在某些環境中,局部變量可以修改全局變量:
舉個栗子:
names = ['Rose','Jcak']
def change_name():
names[0] = 'kitter'
print("Idide function",names) # ====》 Idide function ['kitter', 'Jcak']
change_name()
print(names) # ====》 ['kitter', 'Jcak'] 全局變量被修改了
註意:在當全局變量是數字或者是字符串的時候是不能修改的,修改全局變量只在全局變量是列表、字典、集合等可變數據類型中才可以修改。
二、遞歸函數
定義:在函數內部,可以調用其他函數。如果一個函數在內部調用自身本身,這個函數就是遞歸函數。
遞歸函數特性:
- 必須有一個明確的結束條件;
- 每次進入更深一層遞歸時,問題規模相比上次遞歸都應有所減少
- 相鄰兩次重復之間有緊密的聯系,前一次要為後一次做準備(通常前一次的輸出就作為後一次的輸入)。
- 遞歸效率不高,遞歸層次過多會導致棧溢出(在計算機中,函數調用是通過棧(stack)這種數據結構實現的,每當進入一個函數調用,棧就會加一層棧幀,每當函數返回,棧就會減一層棧幀。由於棧的大小不是無限的,所以,遞歸調用的次數過多,會導致棧溢出)
舉個栗子:計算1到100之間相加之和
運用之前的for 循環實現:
def sum_add(n):
sum = 0
for i in range(1,n+1): # for循環相加
sum += i
return sum
print("循環求和:",sum_add(100))
運用遞歸函數實現:
def sum(n):
if n > 0:
return n + sum(n - 1) # 通過對函數的多次調用
else:
return 0
print("遞歸求和:",sum(100))
遞歸函數的優點是定義簡單,邏輯清晰。理論上,所有的遞歸函數都可以寫成循環的方式,但循環的邏輯不如遞歸清晰。
註意:使用遞歸函數需要註意防止棧溢出。在計算機中,函數調用是通過棧(stack)這種數據結構實現的,每當進入一個函數調用,棧就會加一層棧幀,每當函數返回,棧就會減一層棧幀。由於棧的大小不是無限的,所以,遞歸調用的次數過多,會導致棧溢出,比如上面的遞歸求和,計算sum(1000)。
C:\Python37\python3.exe D:/pythoncode/Exercise/Exer8/Exer8-14.py
Traceback (most recent call last):
File "D:/pythoncode/Exercise/Exer8/Exer8-14.py", line 16, in <module>
print("遞歸求和:",sum(1000))
File "D:/pythoncode/Exercise/Exer8/Exer8-14.py", line 12, in sum
return n + sum(n - 1)
File "D:/pythoncode/Exercise/Exer8/Exer8-14.py", line 12, in sum
return n + sum(n - 1)
File "D:/pythoncode/Exercise/Exer8/Exer8-14.py", line 12, in sum
return n + sum(n - 1)
[Previous line repeated 994 more times]
File "D:/pythoncode/Exercise/Exer8/Exer8-14.py", line 11, in sum
if n > 0:
RecursionError: maximum recursion depth exceeded in comparison
Process finished with exit code 1
解決遞歸調用棧溢出的方法是通過尾遞歸優化,事實上尾遞歸和循環的效果是一樣的,所以,把循環看成是一種特殊的尾遞歸函數也是可以的。
尾遞歸是指,在函數返回的時候,調用自身本身,並且,return語句不能包含表達式。這樣,編譯器或者解釋器就可以把尾遞歸做優化,使遞歸本身無論調用多少次,都只占用一個棧幀,不會出現棧溢出的情況。
三、匿名函數
匿名函數的命名規則,用lamdba 關鍵字標識,冒號(:)左側表示函數接收的參數(a,b) ,冒號(:)右側表示函數的返回值(a+b)。因為lamdba在創建時不需要命名,所以,叫匿名函數
舉個栗子:
# 普通函數
def add(a,b):
return a + b
print( add(2,3) )
# 匿名函數
add = lambda a,b : a + b
print( add(2,3) )
註意:匿名函數lambda後面可以跟的表達式不能復雜,可以是三元運算符,但是不能是for循環語句
num = lambda n : 3 if n > 5 else n
print(num(3)) ====》 3
res = filter(lambda n : n > 5 ,range(10)) # filter 用於基於條件過濾
for i in res:
print(i) ====》 6 7 8 9
四、內置函數
python內置了一系列的常用函數,以便於我們使用:
具體每個內置函數的用法還得多看多練習。
五、函數式編程
函數式編程是一種編程範式,我們常見的編程範式有命令式編程(Imperative programming),函數式編程,常見的面向對象編程是也是一種命令式編程。
命令式編程是面向計算機硬件的抽象,有變量(對應著存儲單元),賦值語句(獲取,存儲指令),表達式(內存引用和算術運算)和控制語句(跳轉指令),一句話,命令式程序就是一個馮諾依曼機的指令序列。
而函數式編程是面向數學的抽象,將計算描述為一種表達式求值,一句話,函數式程序就是一個表達式。函數式編程關心數據的映射,命令式編程關心解決問題的步驟,這也是為什麽“函數式編程”叫做“函數式編程”。
函數式編程有什麽好處呢?
1)代碼簡潔,易懂。
2)無副作用
由於命令式編程語言也可以通過類似函數指針的方式來實現高階函數,函數式的最主要的好處主要是不可變性帶來的。沒有可變的狀態,函數就是引用透明(Referential transparency)的和沒有副作用(No Side Effect)。
函數式編程的一個特點就是,允許把函數本身作為參數傳入另一個函數,還允許返回一個函數!
參考鏈接:
https://www.jianshu.com/p/17a9d8584530
Python函數初識二