談談裝飾器的實現原理

  • 2019 年 10 月 3 日
  • 筆記

關於我
一個有思想的程式猿,終身學習實踐者,目前在一個創業團隊任team lead,技術棧涉及Android、Python、Java和Go,這個也是我們團隊的主要技術棧。
Github:https://github.com/hylinux1024
微信公眾號:終身開發者(angrycode)

談談裝飾器(Decorator)的實現原理

熟悉Java編程的程式猿對裝飾器模式一定不陌生,它是能夠動態的給一個類添加新的行為的一種設計模式。相對於通過繼承的方式使用裝飾器會更加靈活。

圖片來源wiki百科

Python裡面裝飾器(Decorator)也是一個非常重要的概念。跟裝飾器模式類似,它能夠動態為一個函數、方法或者類添加新的行為,而不需要通過子類繼承或直接修改函數的程式碼來獲取新的行為能力,使用Decorator的方式會更加Pythonic

要理解裝飾器我們就要從函數說起。

0x00 函數

Python中函數是作為一級對象存在的(一切都是對象),它擁有自己的屬性,函數名可以賦值給一個變數,也可以作為另一個函數的參數進行傳遞。

1、定義函數
def fib(n):      """列印小於 n 的 fibonacci 數列"""      a, b = 0, 1      while a < n:          print(a, end=' ')          a, b = b, a + b      print()    def call_fib(func):      """函數名作為函數的參數進行傳遞"""      func(1000)    if __name__ == '__main__':      print(fib)  # <function fib at 0x103e66378>      print(isinstance(fib, object))  # 函數是一級對象:True      print(fib.__name__)  # 函數名稱:fib      print(fib.__code__.co_varnames)  # __code__屬性是函數中的'程式碼對象',co_varnames是函數中的本地變數,以元組的方式展現:('n', 'a', 'b')      print(fib.__doc__)  # 函數中的注釋      print(fib.__globals__)  # 全局變數        f = fib  # 函數賦值給一個變數f      f(1000)  # 通過變數f對函數fib調用        call_fib(fib) # 函數名作為參數
2、嵌套函數

在定義函數時,在函數內部定義一個函數。

def outer_func():      # 在函數內部定義一個函數      def inner_func():          print('inner func')        inner_func()      print('outer func')

嵌套函數對我們理解裝飾器非常重要,也是閉包實現的基礎,這裡先引出了本地變數和全局變數的概念,後文會詳細說明閉包的概念。

3、全局變數(globals)和本地變數(locals)

根據作用域的不同,變數可以分成全局變數和本地變數,這其實是相對的概念。例如在下面的模組中gvar是一個全局變數,而在函數outer_func()定義的是本地變數。

gvar = 'global var' # 全局變數      def outer_func():      gvar = 'outer var' # outer_func 本地變數        # 在函數內部定義一個函數      def inner_func():          gvar = 'inner var' # inner_func 本地變數          print('inner: {}'.format(gvar))        inner_func()      print('outer: {}'.format(gvar))    outer_func()  print('gvar in global : {}'.format(gvar))  # 輸出結果  # inner: inner var  # outer: outer var  # gvar in global : global var  

在函數外定義了全局變數gvar,同時在outer_func()函數中也定義了gvar,而這個是本地變數
從示例程式碼中可以看到,outer_func()並沒有改變全局變數的值。

在函數中定義的變數都存儲在本地符號表(local symbol table)里,同樣inner_func()中的gvar也存在它自己的本地符號表中,而全局變數gvar是則存儲在全局符號表(global symbol table)。
變數的查找路是:首先從本地符號表中查找,然後是外部函數(在嵌套函數中)的本地符號表中查找,再到全局符號表,最後是內置符號表

graph TD  A[本地符號表]-->B[外部函數的本地符號表]  B[函數外的本地符號表]-->C[全局符號表]  C[全局符號表]-->D[內置符號表]

如果把上面程式碼中的inner_func()中的gvar = 'inner var'注釋掉,那麼輸出的結果將是

# inner: outer gvar # inner_func中引用的gvar變數是outer_func中定義的  # outer: outer gvar  # gvar in global : global var

變數查找邏輯可以簡單理解為:就近查找
如果在以上路徑中都沒有找到,Python解析器將拋出NameError: name 'gvar' is not defined

若在函數中要使用全局變數,那麼就需要用到global關鍵字。
對上文的程式碼修改如下

gvar = 'global var'      def outer_func():      global gvar  # 聲明gvar是全局變數      gvar = 'outer gvar'        # 在函數內部定義一個函數      def inner_func():          gvar = 'inner gvar'  # 這個依然是本地變數          print('inner: {}'.format(gvar))        inner_func()      print('outer: {}'.format(gvar))    outer_func()  print('gvar in global : {}'.format(gvar))    # 輸出結果  # inner: inner gvar  # outer: outer gvar  # gvar in global : outer gvar

除了global還有一個nonlocal的關鍵字,它的作用是在函數中使用外部函數的變數定義(注意:不能是全局變數)

gvar = 'global var' # 全局變數      def outer_func():      gvar = 'outer gvar' # 本地變數      # 在函數內部定義一個函數      def inner_func():          nonlocal gvar  # nonlocal的含義是讓gvar使用外部函數的變數,          # 如果外部函數沒有定義該變數,那麼運行時將拋出SyntaxError: no binding for nonlocal 'gvar' found          gvar = 'inner gvar'  # 這個依然是本地變數          print('inner: {}'.format(gvar))        inner_func()      print('outer: {}'.format(gvar))    # 輸出結果  # inner: inner gvar  # outer: inner gvar  # gvar in global : global var

inner_func()中使用nonlocal關鍵字聲明的gvar必須在外部函數(即outer_func()函數)定義,否則將拋出SyntaxError: no binding for nonlocal 'gvar' found

0x01 什麼是閉包

首先結合前文的嵌套函數定義的例子,修改一下程式碼,返回內部函數的對象。

# 普通的嵌套函數  def outer_func():      # 在函數內部定義一個函數      def inner_func():          print('inner func')        inner_func()      print('outer func')      # 閉包  def closure_func():      local_var = 100        def inner_func():          print('inner func call : {}'.format(local_var))        return inner_func # 這裡將形成一個閉包    f = closure_func()  print(f)  print(f.__closure__)  print(outer_func)  print(outer_func.__closure__)      # 輸出結果  # <function closure_func.<locals>.inner_func at 0x104f8a8c8>  # (<cell at 0x104f6fa68: int object at 0x104d23910>,)  # <function outer_func at 0x1070ea268>  # None # 普通函數的__closure__屬性為空

可以看出變數f就是閉包,它是一個函數對象,這個函數可以持有本地變數local_var,而這個本地變數可以脫離定義它的作用域而存在

現在來維基百科關於閉包的定義

在電腦科學中,閉包(英語:Closure),又稱詞法閉包(Lexical Closure)或函數閉包(function closures),是引用了自由變數的函數。這個被引用的自由變數將和這個函數一同存在,即使已經離開了創造它的環境也不例外。–引用自維基百科

0x02 實現裝飾器

有了前面的鋪墊,理解裝飾器就非常簡單啦。
一個函數返回另外一個函數,通常會使用@wrapper的語法形式,而裝飾器其實就是一種語法糖(syntactic sugar)。
我們還是看程式碼

# 定義一個logger函數,在函數執行前列印log資訊  def logger(func):      def log_func(*args):          print('Running "{}" with arguments {}'.format(func.__name__, args))          return func(*args)        return log_func # 形成閉包    # 定義加法函數  def add(x, y):      return x + y    # 以下兩種方式的使用是等價的,當然使用@logger更加Pythonic  @logger  def add(x, y):      return x + y      # add = logger(add)    print(add(1,4))    # 輸出結果  # Running "add" with arguments (1, 4)  # 5

這樣的通過自定義裝飾器,我們就可以動態地給函數添加新的功能。
除了自定義的裝飾器,還有常見的如classmethod()staticmethod()內置的裝飾器。

0x03 總結

本文重點說明函數和嵌套函數的定義,還說明了全局變數和本地變數的作用域,在Python中變數索引的路徑是就近查找,同時引出閉包是一個持有自由變數的函數對象的概念,而通過閉包可以實現裝飾器,在使用使用裝飾器時,可以使用@wrapper形式的語法糖。

0x04 引用

  1. https://docs.python.org/3/reference/compound_stmts.html#function-definitions
  2. https://wiki.python.org/moin/PythonDecorators
  3. https://docs.python.org/3/tutorial/controlflow.html#defining-functions
  4. https://zh.wikipedia.org/wiki/%E9%97%AD%E5%8C%85_(%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6)