Python基礎(十三)

  • 2019 年 10 月 3 日
  • 筆記

今日主要內容

  • 閉包
  • 裝飾器初識
  • 標準裝飾器

一、閉包

(一)什麼是閉包

  1. 閉包:內層函數調用外層函數的變量就是閉包(不能是全局變量)

    def func1():   a = 10   def func2():       print(a)  # 內層函數調用外層函數的變量,這就是一個閉包    func1()
  2. 檢測閉包的方法

    • 函數名.__closure__
    • 若返回對象地址就是一個閉包,返回None就不是一個閉包
    • 注意:.__closure__檢測的是函數名,判斷這個函數是否是閉包
    def func1():   a = 10   def func2():       print(a)  # 調用外層變量a,是閉包   print(func2.__closure__)  # 判斷func2是否是閉包    func1()    運行結果:  (<cell at 0x00000230F21874F8: int object at 0x0000000063258190>,)
    def func1():      a = 10      def func2():          a = 10          print(a)  # 使用自身函數變量a,不是閉包      print(func2.__closure__)    func1()    運行結果:  None
  3. 如何在全局空間中調用內部函數

    • 外層函數返回內層函數的函數名(內存地址),就可在全局空間中調用內部函數
    def func1():   a = 10   def func2():       print(a)   return func2  # 返回內層函數的函數名    f = func1()  # 此時f就是func2  f()  # 調用內層函數func2  print(f.__closure__)    運行結果:  10  (<cell at 0x00000280A87574F8: int object at 0x0000000063258190>,)

(二)閉包的作用

  1. 保護數據安全
  • 說白話一點,如果數據放在全局變量中(頂格寫代碼),數據的安全性很低,因為所有人都可以無意間修改它,想解決這個問題我們可以把數據放到函數中,只要不調用這個函數,我的數據就是安全的。但是有個問題,每次執行函數完,解釋器就會自動清空此函數開闢的局部空間中所有內容(包括數據開闢的空間),所以每次調用完函數,我的數據就沒了,此時就需要利用到了閉包。閉包可以在內層函數調用外層函數中的變量,而且內層函數如果調用了外層函數中的變量,那這個變量將不會消亡,將會常駐內存。所以,我們只需要在全局空間中調用這個閉包的內層函數,就可以使用我的數據了,而且數據的安全性也提高了。
  1. 將變量常駐內存,供後續代碼使用

    def outer():   lst = [1,2,3,4,5]   def inner():       return lst   return inner    f = outer()  f_lst = f()  print(f_lst)    運行結果:  [1,2,3,4,5]

(三)閉包的應用

  • 防止數據被誤修改
  • 裝飾器(與閉包格式相同)

二、裝飾器初識

(一)軟件開發的六大原則(了解)

  1. 開閉原則(Open Close Principle)——裝飾器依據
  2. 里氏代換原則(Liskov Substitution Principle)
  3. 依賴倒轉原則(Dependence Inversion Principle)
  4. 接口隔離原則(Interface Segregation Principle)
  5. 迪米特法則(最少知道原則)(Demeter Principle)
  6. 合成復用原則(Composite Reuse Principle)

(二)裝飾器依據——開閉原則

  • 開放封閉原則:
    • 對功能擴展開放
    • 對源碼修改封閉

(三)裝飾器引入

  • 相信大多數人都玩LOL,我們模擬一次遊戲過程:

    • 向平常一樣的流程,十連跪…
    def play_lol():    print("登陸遊戲")    print("開始排位...")    print("遊戲中...")    print("失敗...")    print("結束遊戲")    play_lol()    運行結果:  登陸遊戲  開始排位...  遊戲中...  Virtory  結束遊戲
    • 受不了了,開一個外掛吧,此時我的函數需要擴展,在前後添加開啟關閉外掛的功能
    def play_lol():    print("開啟外掛!")  # 添加開啟外掛功能    print("登陸遊戲")    print("開始排位...")    print("遊戲中...")    print("勝利!!!")    print("結束遊戲")    print("關閉外掛!")  # 添加關閉外掛功能    play_lol()    運行結果:  開啟外掛!  登陸遊戲  開始排位...  遊戲中...  勝利!!!  結束遊戲  關閉外掛!
    • 但此時,違背了開閉原則,對源代碼進行了修改。想一個方法,對源代碼進行前後包裝,不改變源碼
    def play_lol():    print("登陸遊戲")    print("開始排位...")    print("遊戲中...")    print("勝利!!!")    print("結束遊戲")    def new_play():  # 將上面代碼前後包裝成了一個新的函數,沒有改變源碼    print("開啟外掛!")    play_lol()    print("關閉外掛!")    new_play()    運行結果:  開啟外掛!  登陸遊戲  開始排位...  遊戲中...  勝利!!!  結束遊戲  關閉外掛!
    • 功能實現了,而且還沒有改變源碼,但是有一個問題,我們之前訪問調用的是play_lol這個函數,但此時我們訪問調用的是new_play()這個函數,相當於改變了調用,還是違背了開閉原則,沒有達到擴展的效果,此時我們就需要對這段代碼稍作變化
    def play_lol():    print("登陸遊戲")    print("開始排位...")    print("遊戲中...")    print("勝利!!!")    print("結束遊戲")    def wrapper(fn):    # 裝飾器雛形    def inner():        print("開啟外掛!")        fn()        print("關閉外掛!")    return inner    func = wrapper(play_lol)  # 調用裝飾器函數將我基礎函數傳入進去包裝  play_lol = func  # 返回的是包裝函數,將包裝函數重命名成我原來的函數  play_lol()  # 調用此函數    運行結果:  開啟外掛!  登陸遊戲  開始排位...  遊戲中...  勝利!!!  結束遊戲  關閉外掛!
    • 上述代碼就引出了裝飾器的雛形,刨析一下:
      • 裝飾器雛形:與閉包的格式相同,兩層函數構成:
        • 內層函數就是我的包裝函數,將擴展的功能和原函數包在一起組成一個函數
        • 外層函數的作用就是給內層函數傳參用的,傳入的是我原函數的函數名,在內層調用
      • 裝飾器的返回值 return inner:裝飾器的返回值是內層函數的函數名,真正進行包裝擴展的是內層函數
      • 將返回值重命名成原函數名:將返回值重命名成原來的函數名,其實就是把真正作用的包裝函數重命名成原來的函數名,所以就解決了調用新函數的問題,真正遵循了開閉原則,再次調用原來的函數其實真正運行的是裝飾器內部的inner函數
    def wrapper(fn):    # 裝飾器雛形    def inner():        print("開啟外掛!")        fn()        print("關閉外掛!")    return inner    func = wrapper(play_lol)  # 調用裝飾器函數將我基礎函數傳入進去包裝  play_lol = func  # 返回的是包裝函數,將包賺函數重命名成我原來的函數  play_lol()  # 調用此函數
    • 利用語法糖裝飾

      • 使用裝飾器的兩行代碼可以轉換成語法糖
      func = wrapper(play_lol)  # 調用裝飾器函數將我基礎函數傳入進去包裝  play_lol = func  # 返回的是包裝函數,將包賺函數重命名成我原來的函數
      • 語法糖
      @wrapper  # 語法糖  def play_lol():      print("登陸遊戲")      print("開始排位...")      print("遊戲中...")      print("勝利!!!")      print("結束遊戲")
    • 此時,裝飾器的雛形就出來了

  • 裝飾器雛形

    def wrapper(fn):    def inner():        """擴展功能"""        fn()        """擴展功能"""    return inner    @wrapper  def func():    pass    func()
  • 遊戲模擬繼續進行

    • 就算開掛,我們也得選完英雄,才能進入遊戲,所以我們給基礎函數傳個參數,但是我們真正執行的是裝飾器內部的inner包裝函數,所以也要給inner傳入參數
    def wrapper(fn):    # 裝飾器雛形    def inner(hero):  # 套到裝飾器中內層包裝函數參數        print("開啟外掛!")        fn(hero)  # 基礎函數參數        print("關閉外掛!")    return inner    @wrapper  def play_lol(hero):  # 基礎函數參數    print("登陸遊戲")    print("開始排位...")    print(f"選擇英雄:{hero}")    print("遊戲中...")    print("勝利!!!")    print("結束遊戲")    play_lol("蓋倫")    運行結果:  開啟外掛!  登陸遊戲  開始排位...  選擇英雄:蓋倫  # 傳入的參數  遊戲中...  勝利!!!  結束遊戲  關閉外掛!
    • 雖然開掛了,但是還是會遇到巨坑無敵坑的隊友,我們得把他記下來舉報他,所以要給基礎函數填寫返回值,同時給真正執行的inner函數填寫返回值
    def wrapper(fn):    # 裝飾器雛形    def inner(hero):        print("開啟外掛!")        ret = fn(hero)  # 接收基礎函數的返回值        print("關閉外掛!")        return ret  # 返回包裝後的函數的返回值    return inner    @wrapper  def play_lol(hero):  # 基礎函數參數    print("登陸遊戲")    print("開始排位...")    print(f"選擇英雄:{hero}")    print("遊戲中...")    print("勝利!!!")    print("結束遊戲")    return "坑比隊友:xxx"  # 基礎函數返回值    print(play_lol("蓋倫"))    運行結果:  開啟外掛!  登陸遊戲  開始排位...  選擇英雄:蓋倫  遊戲中...  勝利!!!  結束遊戲  關閉外掛!  坑比隊友:xxx  # 返回值
    • 到此,我們裝飾器標準模式也就出來了
  • 裝飾器標準模式(非常重要)

    def wrapper(fn):    def inner(*args, **kwargs):        """擴展功能"""        ret = fn(*args, **kwargs)        """擴展功能"""        return ret    return inner    @wrapper  def func():    pass    func()