Pythin語言的函數(下)
前情回顧:
本篇文章是我對 Pythin語言的函數 理解的最後一講,在開始進入本篇內容之前我們先回顧一下前面我都介紹了 Pythin語言的函數 的哪些內容:
-
什麼是函數,它能做什麼?
答:Python一切皆對象,函數也是一個 對象。它的功能是用來保存一些可執行的代碼,並且可以在需要時,對這些語句進行 多次調用。 -
函數怎麼創建?
答:創建函數的語法
def 函數名([形參1,形參2,形參3....]):
代碼塊
-
函數的參數有哪些形式?他們是怎麼傳遞的?
答:函數的參數就兩種:形參 和 實參;
形參(形式參數) 定義:形參就相當於在函數內部聲明瞭 變量,但是 並不是賦值;
實參(實際參數) 指定對應 了函數在 創建時定義 的形參。在調用函數時必須傳遞 實參,實參將會賦值給對應的形參,簡單來說:有幾個形參就要有幾個實參。
創建函數時需定義好函數需要哪些 形參;在調用函數時將傳遞進來的 實參 與創建函數時定義好的 形參 11對應,以便函數能正常的運轉;如果,對傳遞進來的 實參 的數量無法確定,那麼在創建函數時要將 形參 定義成 不定長參數(*) 。 -
什麼是函數的 返回值?print() 不能做函數的返回值嗎?
答:返回值就是函數執行以後返回的結果;通過 關鍵字return 來指定函數的返回值;我們可以通過一個變量來接收函數的返回值,或者可以直接函數來使用函數的返回值。
print() 方法用於打印輸出,無法對函數運算後返回的值進行再操作;函數通過 關鍵字return 返回的值是可以進行再操作的。
return 後面可以跟 任意對象 ,返回值 甚至 可以是一個 函數。如果函數裏僅僅只寫一個 return 或者不寫 return ,則相當於函數 return Noun。 -
什麼是函數的作用域?
答:作用域(scope)指的是 變量生效的區域 ;函數在不同的位置它最終訪問到的結果是不一樣的。在Python中一共有兩種作用域:全局作用域 和 函數作用域。對於在 在函數內部 修改 全局變量的操作,我們還學習了一個關鍵字 global 的功能和使用。 -
文檔字符串 help() 是幹嘛的?
答:help() 是 Python中內置函數,我們可以通過help()函數可以查詢Python中 Python內置函數的用法;通過 help()函數 我們能看到 Python內置函數 隱藏 掉了的一些默認值。 -
什麼是 遞歸函數 ?
答:函數的一種經典操作:自己調用自己。
在使用 遞歸函數 時必須要注意的兩點:
1. 基線條件:問題可以被分解爲 最小問題,當滿足基線條件時,遞歸就 不執行 了;
2. 遞歸條件:可以將問題 繼續分解 的條件。 -
在講函數時,順帶着的介紹了一種代碼的執行方式:命名空間。
答:所謂的 命名空間 只是代碼執行的一種方式;代碼在執行的時候是 以命名空間爲單位進行執行 的代碼。
回顧結束,回顧期間如果出現什麼疑問或者不理解的,具體的詳細細節回看前文案例。接下來開始我們函數的最後內容:高階函數、函數的閉/解包 和 什麼是 裝飾器。
1. 高階函數
在開始分享第一個內容之前,我希望大家帶着這麼一個問題來閱讀本篇文章的內容:什麼是 高階函數 或者說是 高級函數?
先回答我爲大家提出的問題: 高階函數 或者說是 高級函數 它有 兩個特點;如果滿足,那麼它就是 高階函數 或者說是 高級函數。
特點一:接收 一個或多個函數 作爲 參數;
特點二:將 函數 作爲 返回值 返回。
接下來我們我們對上面的兩個特點一一的進行詳細的講解。
參考實例:定義一個函數,可以將制定的列表中所有的偶數保存到一個新的列表中返回。
def fn(lst): # 定義一個函數:用來檢測參數 lst 是否爲偶數,並對其進行篩選
new_lst = []
for n in lst:
if n % 2 == 0:
new_lst.append(n)
return new_lst
a = [1,2,3,4,5,6,7,8,9,10]
print(fn(a)) # [2, 4, 6, 8, 10]
編程原則第一條:程序運行沒有報錯。
編程原則第二條:輸出的結果是否符合需求。
大家在自己的編輯器中運行參考實例,得到的結果肯定是符合需求結果的。
那麼,現在新的一個問題來了:上面的參考實例是一個 高階函數 嗎”?
很明顯,它不是,它只是一個能進行正常計算的函數。那它爲什麼不是?因爲 高階函數 的兩個特點它一個都沒滿足。既沒有接收 一個或多個函數 作爲 參數;也沒有將 函數 作爲 返回值 返回,參考實例返回的是一個列表。
OK,我們先放下它“是不是 高階函數 ”這個問題,假如我們現在又有這麼一個需求:定義一個函數,可以將制定的列表中所有的奇數保存到一個新的列表中返回。我們該怎麼實現?
參考實例:
def fn(lst): # 定義一個函數:用來檢測參數 lst 是否爲奇數,並對其進行篩選
new_lst = []
for n in lst:
if n % 2 != 0:
new_lst.append(n)
return new_lst
a = [1,2,3,4,5,6,7,8,9,10]
print(fn(a)) # [1, 3, 5, 7, 9]
假如我們現在又有這麼一個需求:定義一個函數,可以將制定的列表中所有的大於 5 的數保存到一個新的列表中返回。我們該怎麼實現?
參考實例:
def fn(lst): # 定義一個函數:用來檢測參數 lst 是否爲大於 5 的數,並對其進行篩選
new_lst = []
for n in lst:
if n > 5 :
new_lst.append(n)
return new_lst
a = [1,2,3,4,5,6,7,8,9,10]
print(fn(a)) # [6, 7, 8, 9, 10]
假如我們現在又有這麼一個需求:定義一個函數,可以將制定的列表中所有的大於 5 的奇數保存到一個新的列表中返回;
又有這麼一個需求:定義一個函數,可以將制定的列表中所有的大於 8 的偶數保存到一個新的列表中返回;
…………(以上省去10萬字需求)
怎麼實現?
請大家放心,博主我不是純粹的在水字數佔用大家的閱讀空間、浪費大家的閱讀時間,而是帶大家進行思考。畢竟 CSDN 上的上榜文章不是靠字數刷上去的。
但是以上省去10萬字的各種需求,我們該怎麼實現?(記住:我們只能提建議和思路,不要和甲方爸爸講道理)或者說:我們希望我們創建的一個函數能實現多種功能?怎麼辦? 在這個函數裏把所有的需求 都加上 是不是就可以了?必須可以!!!
此時是不是終於繞回到了今天的第一個主題: 高階函數
OK,那麼我們現在先來用 高階函數 實現一個需求:定義一個函數,可以將制定的列表中所有大於5的偶數保存到一個新的列表中返回。
參考實例:
def fn(lst): # 定義一個高階函數
def fn1(i): # 定義一個函數:用來檢測一個數是否爲偶數
if i % 2 == 0:
return True
def fn2(i): # 定義一個函數:用來檢測一個數是否大於5
if i > 5:
return True
return False
nwe_lst = []
for n in lst:
if fn1(n):
if not fn2(n):
nwe_lst.append(n)
return nwe_lst
a = [1,2,3,4,5,6,7,8,9,10]
print(fn(a)) # [2, 4]
現在,甲方爸爸的所有需求,對於我們來說全部實現是沒有問題了。但大家是不是又發現:如果後期需要維護或者修改上面的參考實例,實際操作起來會不會非常非常的麻煩,工作量會不會非常非常的大?那我們該怎麼辦?
現在之所以出現這樣問題的原因是不是我把門最後的 if判斷語句 給 寫死了 呀。我們當然最希望我們創建的這個函數 跟根據不同的情況做出相應的不同的處理 啊;至於這個函數最後處理到什麼情況,是不是應該由 傳遞相應參數的人 來定,而不是我們確定啊。那現在我們能不能做到:當使用我們函數的人給我們創建的函數傳遞什麼信號,我們創建的函數就使用什麼規則呢?當然也是 必須能!!!
參考實例:
def fn1(i): # 定義一個函數:用來檢測一個數是否爲偶數
if i % 2 == 0:
return True
def fn2(i): # 定義一個函數:用來檢測一個數是否大於5
if i > 5:
return True
return False
def fn3(i): # 定義一個函數:用來檢測一個數是否爲3的倍數
if i % 3 == 0:
return True
return False
def fn(func,lst): # 定義一個高階函數
nwe_lst = []
for n in lst:
if func(n):
nwe_lst.append(n)
return nwe_lst
a = [1,2,3,4,5,6,7,8,9,10]
print(fn(fn1,a)) # [2, 4, 6, 8, 10]
print(fn(fn2,a)) # [6, 7, 8, 9, 10]
print(fn(fn3,a)) # [3, 6, 9]
此時,我們是否發現:當我們傳進不同函數的時候,高階函數 的計算規則是不是就變了;我們再回想一下 高階函數 的兩個特點:接收 一個或多個函數 作爲 參數;將 函數 作爲 返回值 返回。
我爲這個 高階函數 寫了這麼多內容,那麼我想問下大家:高階函數 這個操作,有什麼好處呢?
高階函數 它的優勢就在於:我們以前傳遞實參都是 整數(int)、字符串(str)、列表(list) ……這些都是 單一的數據;而當我們使用上了 高階函數 之後,我們就不僅僅只是傳遞一些 單一的數據 了,還能傳遞3行、30行、300行……這些大塊大塊的代碼塊了。此時我們再來回看上面的這麼多的內容,我寫這麼多,還是有點必要的 😃
當然了,對於 高階函數 我們不能因爲它的優勢而對它進行 濫用,也要根據我們 實際的需求 和 程序的運轉效率 合理的選擇使用。
最後給大家留下一個小小的思考題:如果,我想要實現統計列表中 所有大於5的偶數 這個需求,該怎麼操作?
2. 閉包
上面我們介紹了對函數的一種使用方式 高階函數: 接收 一個或多個函數 作爲 參數 進行傳遞;也可以將 函數 作爲 返回值 進行返回。下面呢,我們來介紹函數的第二種使用方式 閉包。
什麼是 閉包 呢?將 函數 作爲 返回值 進行返回 的這種操作我們也稱爲 閉包。
咦,對 閉包 的介紹,大家是不是很眼熟。沒錯,確實是 高階函數 的第二特點。即然前面已經介紹過 高階函數 的第二特點了,再搞一個 閉包 它又有什麼用呢?接下來和大家一起看一個實例:
def fn():
def fn1(): # 在函數內部定義一個函數
print('我是fn1')
return fn1 # 將函數作爲 返回值 返回
print(fn()) # <function fn.<locals>.fn2 at 0x00000221EBA60940>
咦,爲什麼打印的是 函數fn 的 屬性信息,而不是 我是fn1?在這裏呢,我們再做一個小小的 前情回顧:print(*objects, sep=' ', end='\n', file=sys.stdout, flush=False)
函數print() 它打印的是一個 對象信息;上面實例 函數print() 返回的是 內部函數fn2(),因爲 返回值return 指向的是一個 函數對象fn1。
如果我們想要返回 我是fn1 該怎麼辦?我們需要通過自定義一個 變量,然後對 函數fn 進行調用,再然後打印調用到 函數fn 結果的 變量 就會得到
我是fn1 了。
參考實例:
def fn():
def fn2(): # 在函數內部定義一個函數
print('我是fn2')
return fn2 # 將函數作爲 返回值 返回
print(fn()) # <function fn.<locals>.fn2 at 0x00000221EBA60940>
r = fn()
print(r()) # 我是fn2
# None <--不需要打印
r() # 我是fn2 <--直接調用就可以了
變量r 如何從一個沒有值的變量變成了一個 函數?是因爲 變量r 調用 高階函數fn() 後 返回的函數,變量r 調用到的這個函數是在 高階函數fn() 內定義的,並不是 全局函數。
對函數的作用域 我們在上一篇文章 9. Pythin語言的函數(中)裏講到過:
作用域指的是 變量生效的區域 ;全局變量可以在程序的任意位置進行訪問,而局部變量它只能在函數內部被訪問。再簡單一點的總結就是:函數中的變量從裏向外 看 是 可以 的,但是 無法 從外向裏 看 的。
簡單的回顧完函數的案例後,我們再回到參考實例:即然前面說 變量r 不是 全局函數,那麼 函數執行r() 是不是就可以訪問到 函數fn 中的變量。
參考實例:
def fn():
a = 10
def fn1(): # 在函數內部定義一個函數
print('我是fn2',a)
return fn1 # 將函數作爲 返回值 返回
r = fn()
r() # 我是fn2 10
print(a) # NameError: name 'a' is not defined
上面 參考實例 這種對 高階函數 調用的方式,我們統稱之爲 閉包。
此時,我們又要問了,不是已經有一個 高階函數 了嗎?爲什麼我們還要再多一個 閉包 這種怪怪的名詞啊?是來搞大家的心態嗎?其實不是的,接下來聽我娓娓道來。
回到 參考實例,高階函數 內的 變量a 是在 高階函數fn 內部的,只能通過 r函數 才能獲取到 變量a 的值,因爲 r函數 不是 全局函數。那此時我們是不是可以這麼理解:整個 高階函數fn 是一個封閉的函數包,想要獲取 高階函數fn 內的信息,只能通過 r函數 來獲取。
總結一下:
閉包的好處:
1)通過閉包可以創建一些只有 當前函數 能訪問的 變量;
2)可以將一些 私有數據 藏到 閉包中(安全)。
接下來,我們通過一個 “求多個數的平均值” 這麼一個案例來講解一下 閉包的好處。
我們先拋開所有,先單純看下,怎麼實現需求。參考實例:
nums = [50,60,80,90,120]
# sum() 用來求一個列表中元素之和
print(sum(nums)/len(nums)) # 80.0
OK,通過上面這個參考實例我們已經知道 “5個數的平均值” 怎麼求了;那如果出現 6個數、10個數、100個數、100000個數……的平均值該怎麼實現呢?來,接下來我們繼續完善。
nums = []
# 定義一個函數 計算平均值
def average(n):
# 將n添加到列表中
nums.append(n)
# 求平均值
return sum(nums) / len(nums)
print(average(10)) # 10.0
print(average(20)) # 15.0
print(average(30)) # 20.0
上面的參考實例雖然對 多個隨機數 的輸入方式還比較麻煩,但是對 “求多個數的平均值” 這個需求我們基本上還是實現了的。
現在我們來看一下上面的代碼是否還有什麼紕漏:在參考實例裏nums = []
是一個全局變量,意味着我們可以在 任意位置 都可以訪問到nums = []
;也意味着你能看得見、訪問的到,別人同樣也可以看得見、訪問的到;你能修改nums = []
,別人也能修改nums = []
。
未來,在我們參加工作,開始和不同的人合作開發軟件的時候,大家多少都會出現一些錯誤。比如說:
nums = []
# 定義一個函數 計算平均值
def average(n):
# 將n添加到列表中
nums.append(n)
# 求平均值
return sum(nums) / len(nums)
print(average(10))
nums.append('Python') # <---
print(average(20))
print(average(30))
#-----------------------------------------------------------------------------------
nums = []
nums = ['a','b',] # <---
# 定義一個函數 計算平均值
def average(n):
# 將n添加到列表中
nums.append(n)
# 求平均值
return sum(nums) / len(nums)
print(average(10))
print(average(20))
print(average(30))
各種不同的奇葩的問題,因爲大量的代碼而出現 一點也不足爲奇。而因爲這些細小的問題導致你的工作報廢,更甚者導致了整個程序的奔潰,這樣的問題是不是很可怕。此時,我們是否是希望nums = []
別誰都能隨便更改,或者說:最起碼別讓它就這麼四仰八叉的“裸露”在外面。
此時,就非常的需要函數的 閉包 了。而只要使用了函數的 閉包 就一定會使用到 函數的嵌套。
函數嵌套:
1)將內部函數作爲返回值返回
2)內部函數必須要使用到外部函數的變量
參考實例:
def make_average():
nums = []
def average(n): # 定義一個函數 計算平均值
nums.append(n) # 將n添加到列表中
return sum(nums) / len(nums) # 求平均值
return average
nums = make_average()
print(nums(10)) # 10
print(nums(20)) # 15
print(nums(30)) # 20
print(nums(40)) # 25
最後,我們總結一下 形成閉包的條件:
1)函數嵌套;
2)將內部函數作爲返回值返回;
3)內部函數必須要使用到外部函數的變量;你需要用到,但你又不希望別人使用的變量,確保變量的安全;不然你廢起白咧的寫這麼多,還不太好領悟到的代碼做什麼。
3. 裝飾器
裝飾器 字面理解:一種用來進行 裝飾 的工具。
這麼解釋這個名詞還是很抽象了,那我們來舉個例子,通過 “打比方,舉例子” 來讓大家能更好的理解這個名詞。
現在社會,家家戶戶基本上都有屬於自己的房子;現在各種什麼房產中介啊,或者什麼售樓中心啊都會推出什麼 “精裝修房” 或者什麼 “簡裝修房”;不過呢,羊毛出在羊身上,在以前買賣房子都沒這些修的,直接就是 “樣板房”,一個大屋殼子,裏面空蕩蕩的,什麼都沒有;此時,這樣的房子是不是還不能住人的,買完房子後再怎麼沒錢,我們最少要 買一張牀 吧;牆壁 灰撲撲的,最少要 用白石灰擦一擦 吧……以上行爲等等等等,我們都統稱這些行爲叫做 裝修。
其實我們 寫代碼 和 蓋房子 的是一樣一樣的,不然我們也不會被稱之爲 碼農 了,唯一不同的可能就是我們的工作場景還是有區別的吧。迴歸正題,即然新蓋好的房子需要 裝修,那我們剛寫好還冒着熱氣的代碼是不是也需要 裝飾裝飾。
例子 說完了,接下來開始介紹 Python語言的裝飾器,我們該怎麼使用。
3. 1 裝飾器的引入
在開始介紹 裝飾器的引入 之前,我們先定義一個 有意義 的函數,什麼叫 有意義 的函數?或者說 我們之前寫的哪些函數難道都沒有什麼意義嗎?
我們這裏說的 有意義 不是說 我們前面寫的代碼或者說案例能不能用;而是說 我們給自己寫的代碼塊進行命名,在未來我們可能會反覆的需要之前寫過的那些內容的時候,我們可以靈活的反覆調用 這樣的 代碼塊 我們稱之爲 有意義 的函數。
我們現在來定義兩個 有意義 的函數。參考實例:
def nums_add(*n): # 求任意數的 和
i = 0
for a in n:
i += a
return i
def nums_mul(*n): # 求任意數的 積
i = 1
for a in n:
i *= a
return i
# 打印測試一下是否有誤:
r = nums_add(1, 2, 3)
c = nums_mul(2, 3, 4)
print(r) # 6
print(c) # 24
接下來呢我們對我們剛定義的兩個 有意義 的函數添加一些需求,什麼需求呢:我希望函數在計算前出現一句提示,叫“計算開始”;等函數計算結束後出現一句提示,叫“計算結束”。我們該怎麼進行操作?參考實例:
def nums_add(*n): # 求任意數的 和
print('計算開始...')
print('計算結束:')
i = 0
for a in n:
i += a
return i
def nums_mul(*n): # 求任意數的 積
print('計算開始...')
print('計算結束:')
i = 1
for a in n:
i *= a
return i
# 打印測試一下是否有誤:
r = nums_add(1, 2, 3)
print(r) # 計算開始...
# 計算結束:
# 6
c = nums_mul(2, 3, 4)
print(c) # 計算開始...
# 計算結束:
# 24
需求效果現在是已經實現了,但是我們寫的代碼是否有點怪怪的。代碼 print('計算開始...') 和 print('計算結束:')
在源碼裏是否重複的出現了兩次了。我們可以直接通過修改函數中的代碼來完成一些需求,但是會產生一些問題。會出現哪些問題:
問題1) 我們現在只是兩個函數還好,如果出現了 n個函數 都有一樣的需求,該怎麼辦?換個表達方式 如果要 修改的函數比較多,修改起來 非常麻煩;
問題2) 不方便 後期維護;
問題3) 這樣會違反 程序設計的開閉原則(OCP);程序的設計要求 開放對程序的擴展,要關閉對程序的修改。
現在回到上面的實例,上面實例是否完全的違反了 程序的設計開閉原則(OCP)。那我們現在能否在儘可能的 不修改 源程序(源代碼)的基礎上,對 新增的需求 進行 擴展。參考實例:
def nums_add(*n): # 求任意數的 和
i = 0
for a in n:
i += a
return i
def nums_mul(*n): # 求任意數的 積
i = 1
for a in n:
i *= a
return i
def fn(funs): # 甲方爸爸後續添加的需求
print('函數開始運算...')
print(funs)
return '函數運算結束。'
# 打印測試一下是否有誤:
r = fn(nums_add(1, 2, 3))
print(r) # 函數開始運算...
# 6
# 函數運算結束。
c = fn(nums_mul(2, 3, 4))
print(c) # 函數開始運算...
# 24
# 函數運算結束。
此時,函數fn()
是不是就是 函數nums_add()
和 函數nums_mul()
的 裝飾函數。在不修改 函數nums_add()
和 函數nums_mul()
源代碼 的基礎上對 最後輸出的結果 進行了相應的 調整 或 修改。
當然,現在我們只是爲了實現 實例效果 只是設置了一些簡單的 裝飾函數;但是在未來,我們在參加開發各種大型項目的時候會不會需要很多很多的 功能 或者 效果 需要 引用,此時就是 Python語言的裝飾器 展現它強大力量的時候了。
3.2 裝飾器的使用
上面我們簡單的介紹了 程序的編寫需要符合 程序設計的開閉原則(OCP);什麼是 裝飾器(裝飾函數);爲什麼需要 裝飾器(裝飾函數) 的引入。
接下來我們詳細的來講解一下 裝飾器的使用。
老習慣,先來一個參考實例:
def start_end(): # 用來對其他的函數進行擴展,擴展函數執行的時候 打印 “開始執行” 和 “執行結束”
def new_function(): # 創建一個函數
pass # 佔位
return new_function # 返回一個函數
# 打印測試一下是否有誤:
f1 = start_end()
f2 = start_end()
print(f1) # <function start_end.<locals>.new_function at 0x000002008C240940>
print(f2) # <function start_end.<locals>.new_function at 0x000002008C240A60>
雖然,變量f1
和 變量f2
調用的都是 函數start_end()
,但實際上我們調用的是 函數new_function()
運算後的結果 對不對。現在呢,這裏有個問題:變量f1
和 變量f2
兩次調用的 函數start_end()
是同一個函數嗎?答案呢 肯定不是。雖然,變量f1
和 變量f2
兩次調用的 函數都叫 start_end()
;但是,最後通過打印這兩個變量獲得的函數所在的 內存地址 卻是 完全不一樣。
當然了,上面的問題呢都是一些小插曲;上面的 參考實例 到目前爲止 我們發現我們設置的 變量 調用了 函數start_end()
最後給我們返回了 函數new_function()
就這一個結果,並沒有什麼其他的功能。和我們說的 用函數start_end()
來對其他的函數進行擴展,擴展函數執行的時候 打印 “開始執行” 和 “執行結束” 這個需求還有十萬八千里,最起碼現在還是什麼功能都沒有。那現在我們來對返回的 函數new_function()
里加點東西吧。參考實例:
def fn():
print('我是fn函數')
def start_end(): # 用來對其他的函數進行擴展,擴展函數執行的時候 打印 “開始執行” 和 “執行結束”
def new_function(): # 創建一個函數
print('函數開始運算...')
fn() # <-----
print('函數運算結束。')
return new_function # 返回一個函數
f1 = start_end()
f1() # 函數開始運算...
# 我是fn函數
# 函數運算結束。
通過上面的 參考實例 以及 最後的輸出結果 發現 函數start_end()
已經對 函數fn()
的輸出結果進行了擴展,而且函數運行最後的結果非常的OK啊。那按照這個格式 函數start_end()
也可以對 函數nums_add()
的輸出結果進行了擴展。參考實例:
def nums_add(*n): # 求任意數的 和
i = 0
for a in n:
i += a
return i
def start_end(): # 用來對其他的函數進行擴展,擴展函數執行的時候 打印 “開始執行” 和 “執行結束”
def new_function(): # 創建一個函數
print('函數開始運算...')
nums_add() # <-----
print('函數運算結束。')
return new_function # 返回一個函數
f1 = start_end()
f1()
我們先別在意 參考實例 最後輸出的結果,因爲大家如果仔細看就會發現上面的 參考實例 最後輸出的結果一定是會報錯的;但我之所以還要引用這個帶有錯誤 參考實例 就是想表達:雖然 函數start_end()
可以對 函數fn()
和 函數nums_add()
的輸出結果進行了擴展;但是修改他們的基礎還是在修改 函數start_end()
裏的源代碼;對於 函數start_end()
的操作是不符合 程序設計的開閉原則(OCP) 的。那爲什麼會出現這樣的結果呢?是因爲我們把 函數start_end()
裏 負責擴展的函數 那一塊( fn()、nums_add() )寫死了;是不是 因爲我們不知道要對哪塊代碼進行擴展,所以我們對 負責擴展的函數 那一塊 不能寫死 呀;所以,我們需要對 想要擴展的函數 以 參數 的形式 傳遞進來。現在既然找到問題所在,那讓我們一起來修補這塊的漏洞。
說到這,我們來穿插一個小的新知識點:形參 old 、形參 *args 、形參 **kwargs
形參 old:接收要擴展的函數對象。
形參 *args :接收所有 位置參數 的不定長參數
形參 **kwargs :接收所有的 關鍵字參數
參考實例:
def fn():
print('我是fn函數')
def nums_add(*n): # 求任意數的 和
i = 0
for a in n:
i += a
return i
def start_end(old): # 用來對其他的函數進行擴展,擴展函數執行的時候 打印 “開始執行” 和 “執行結束”
def new_function(*args,**kwargs): # 創建一個函數,接收需要進行擴展的函數 以及 需要進行擴展的函數的形參
# 裝包,把 參數 包裝成一個一個的元組,或者一個一個的字典
print('函數開始運算...')
# 把位置參數拆包成一個元組 或者 把關鍵字參數拆包成一個字典 傳遞進去
result = old(*args,**kwargs) # 接收需要進行擴展的函數 以及 需要進行擴展的函數的形參
print('函數運算結束。')
return result
return new_function
f1 = start_end(fn) # 以後如果再想擴展哪個函數,直接調用 函數start_end() 形成一個新的函數,就是對原有函數的一個新的 擴展
c = f1 #
c() # 函數開始運算...
# 我是fn函數
# 函數運算結束。
f2 = start_end(nums_add) # 以後如果再想擴展哪個函數,直接調用 函數start_end() 形成一個新的函數,就是對原有函數的一個新的 擴展
r = f2(1,2,3,4)
print(r) # 函數開始運算...
# 函數運算結束。
# 10
像我們今天用的 函數start_end(old)
類似於這樣的函數,我們統稱爲 裝飾器。通過裝飾器,可以在不修改原來函數的情況下來對函數進行擴展;在開發中,我們都是通過裝飾器來擴展函數的功能的。
當然,講 裝飾器 絕對不能少不了 @ ,不然就是對 裝飾器 的褻瀆。
參考實例:
def start_end(old): # 參數 old 接收要擴展的函數對象 以及 需要進行擴展的函數的形參
def new_function(*n):
print('函數開始運算...')
result = old(*n) # 參數 old 接收要擴展的函數對象 以及 需要進行擴展的函數的形參
print('函數運算結束。')
return result
return new_function
@start_end
def speak():
print('大家加油')
speak()
# 函數開始運算...
# 大家加油
# 函數運算結束。
回到最初我們介紹 裝飾器 時提到的 裝飾器 解決爲我們在開發過程中解決的幾個問題:
問題1) 如果要 修改的函數比較多,修改起來 非常麻煩;
問題2) 不方便 後期維護;
問題3) 這樣會違反 程序設計的開閉原則(OCP);程序的設計要求 開放對程序的擴展,要關閉對程序的修改。
總結小便條
-
高階函數
• 接收函數作爲參數,或者將函數作爲返回值返回的函數就是高階函數 -
閉包
• 將函數作爲返回值也是高階函數我們也稱爲閉包
• 閉包的好處:
通過閉包可以創建一些只有當前函數能訪問的變量;
可以將一些私有數據藏到閉包中。
• 行成閉包的條件:
函數嵌套;
將內部函數作爲返回值返回;
內部函數必須要使用到外部函數的變量。
-
裝飾器的引入
• 我們可以直接通過修改函數中的代碼來完成需求,但是會產生以下一些問題
• 如果修改的函數多,修改起來會比較麻煩
• 不方便後期的維護
• 這樣做會違反開閉原則(ocp)
• 程序的設計,要求開發對程序的擴展,要關閉對程序的修改 -
裝飾器的使用
• 通過裝飾器,可以在不修改原來函數的情況下來對函數進行擴展
• 在開發中,我們都是通過裝飾器來擴展函數的功能的