10. Python語言的核心編程 · 第十章 Python語言的函數(下)

前情回顧:

  本篇文章是我對 Pythin語言的函數 理解的最後一講,在開始進入本篇內容之前我們先回顧一下前面我都介紹了 Pythin語言的函數 的哪些內容:

  1. 什麼是函數,它能做什麼?
    答:Python一切皆對象函數也是一個 對象。它的功能是用來保存一些可執行的代碼,並且可以在需要時,對這些語句進行 多次調用

  2. 函數怎麼創建?
    答:創建函數的語法

def 函數名([形參1,形參2,形參3....]):
    代碼塊
  1. 函數的參數有哪些形式?他們是怎麼傳遞的?
    答:函數的參數就兩種:形參實參
      形參(形式參數) 定義:形參就相當於在函數內部聲明瞭 變量,但是 並不是賦值
      實參(實際參數) 指定對應 了函數在 創建時定義形參。在調用函數時必須傳遞 實參實參將會賦值給對應的形參,簡單來說:有幾個形參就要有幾個實參
      創建函數時需定義好函數需要哪些 形參;在調用函數時將傳遞進來的 實參 與創建函數時定義好的 形參 11對應,以便函數能正常的運轉;如果,對傳遞進來的 實參 的數量無法確定,那麼在創建函數時要將 形參 定義成 不定長參數(*)

  2. 什麼是函數的 返回值?print() 不能做函數的返回值嗎?
    答:返回值就是函數執行以後返回的結果;通過 關鍵字return 來指定函數的返回值;我們可以通過一個變量來接收函數的返回值,或者可以直接函數來使用函數的返回值。
    print() 方法用於打印輸出,無法對函數運算後返回的值進行再操作;函數通過 關鍵字return 返回的值是可以進行再操作的。
    return 後面可以跟 任意對象 ,返回值 甚至 可以是一個 函數。如果函數裏僅僅只寫一個 return 或者不寫 return ,則相當於函數 return Noun。

  3. 什麼是函數的作用域?
    答:作用域(scope)指的是 變量生效的區域 ;函數在不同的位置它最終訪問到的結果是不一樣的。在Python中一共有兩種作用域:全局作用域函數作用域。對於在 在函數內部 修改 全局變量的操作,我們還學習了一個關鍵字 global 的功能和使用。

  4. 文檔字符串 help() 是幹嘛的?
    答:help() 是 Python中內置函數,我們可以通過help()函數可以查詢Python中 Python內置函數的用法;通過 help()函數 我們能看到 Python內置函數 隱藏 掉了的一些默認值。

  5. 什麼是 遞歸函數
    答:函數的一種經典操作:自己調用自己。
    在使用 遞歸函數 時必須要注意的兩點:
      1. 基線條件:問題可以被分解爲 最小問題,當滿足基線條件時,遞歸就 不執行 了;
      2. 遞歸條件:可以將問題 繼續分解 的條件。

  6. 在講函數時,順帶着的介紹了一種代碼的執行方式:命名空間。
    答:所謂的 命名空間 只是代碼執行的一種方式;代碼在執行的時候是 以命名空間爲單位進行執行 的代碼。

  回顧結束,回顧期間如果出現什麼疑問或者不理解的,具體的詳細細節回看前文案例。接下來開始我們函數的最後內容:高階函數函數的閉/解包 和 什麼是 裝飾器

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
    # print(i)
    return i

def nums_mul(*n):
    x = 1
    for i in n:
        x *= i
    return x
    
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);程序的設計要求 開放對程序的擴展,要關閉對程序的修改

  
  
  

總結小便條

本篇文章主要講了以下幾點內容:

  1. 高階函數
    • 接收函數作爲參數,或者將函數作爲返回值返回的函數就是高階函數

  2. 閉包
    • 將函數作爲返回值也是高階函數我們也稱爲閉包

   • 閉包的好處:
    通過閉包可以創建一些只有當前函數能訪問的變量;
    可以將一些私有數據藏到閉包中。

   • 行成閉包的條件:
    函數嵌套;
    將內部函數作爲返回值返回;
    內部函數必須要使用到外部函數的變量。

  1. 裝飾器的引入
    • 我們可以直接通過修改函數中的代碼來完成需求,但是會產生以下一些問題
    • 如果修改的函數多,修改起來會比較麻煩
    • 不方便後期的維護
    • 這樣做會違反開閉原則(ocp)
    • 程序的設計,要求開發對程序的擴展,要關閉對程序的修改

  2. 裝飾器的使用
    • 通過裝飾器,可以在不修改原來函數的情況下來對函數進行擴展
    • 在開發中,我們都是通過裝飾器來擴展函數的功能的

  本章回顧暫時就到這了,如果還有點暈,那就把文章裏所有引用的案例代碼再敲幾遍吧。拜拜~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章