Python 裝飾器你也會用

程式設計師小城發表於2019-03-15

Python的裝飾器(decorator)是一個很棒的機制,也是熟練運用Python的必殺技之一。裝飾器,顧名思義,就是用來裝飾的,它裝飾的是一個函式,保持被裝飾函式的原有功能,再裝飾上(添油加醋)一些其它功能,並返回帶有新增功能的函式物件,所以裝飾器本質上是一個返回函式物件的函式(確切的說,裝飾器應該是可呼叫物件,除了函式,類也可以作為裝飾器)。

在程式設計過程中,我們經常遇到這樣的場景:登入校驗,許可權校驗,日誌記錄等,這些功能程式碼在各個環節都可能需要,但又十分雷同,通過裝飾器來抽象、剝離這部分程式碼可以很好解決這類場景。

裝飾器是什麼?
要理解Python的裝飾器,首先我們先理解一下Python的函式物件。我們知道,在Python裡一切都是物件,函式也不例外,函式是第一類物件(first-class objects),它可以賦值給變數,也可以作為list的元素,還可以作為引數傳遞給其它函式。

函式可以被變數引用
定義一個簡單的函式:

 

def say_hi(): 
    print('Hi!')
say_hi() 
# Output: Hi!

 

我們可以通過另外一個變數say_hi2來引用say_hi函式:

say_hi2 = say_hi
print(say_hi2)
# Output: <function say_hi at 0x7fed671c4378>

say_hi2()
# Output: Hi!

上面的語句中say_hi2 和 say_hi 指向了同樣的函式定義,二者的執行結果也相同。


函式可以作為引數傳遞給其它函式

def say_more(say_hi_func):
    print('More')
    say_hi_func()

say_more(say_hi)
# Output:
#     More
#     Hi

 

在上面的例子中,我們把say_hi函式當做引數傳遞給say_more函式,say_hi 被變數 say_hi_func 引用。


函式可以定義在其它函式內部

def say_hi():
    print('Hi!')
    def say_name():
        print('Tom')
    say_name()

say_hi()
# Output:
#     Hi!
#     Tom

say_name() # 報錯


上述程式碼中,我們在say_hi()函式內部定義了另外一個函式say_name()。say_name()只在say_hi函式內部可見(即,它的作用域在say_hi函式內部),在say_hi外包呼叫時就會出錯。


函式可以返回其它函式的引用

def say_hi():
    print('Hi!')
    def say_name():
        print('Tom')
    return say_name

say_name_func = say_hi()
# 列印Hi!,並返回say_name函式物件
# 並賦值給say_name_func

say_name_func()
# 列印 Tom


上面的例子,say_hi函式返回了其內部定義的函式say_name的引用。這樣在say_hi函式外部也可以使用say_name函式了。
前面我們理解了函式,這有助於我們接下來弄明白裝飾器。


裝飾器(Decorator)
裝飾器是可呼叫物件(callable objects),它用來修改函式或類。
可呼叫物件就是可以接受某些引數並返回某些物件的物件。Python裡的函式和類都是可呼叫物件。


函式裝飾器,就是接受函式作為引數,並對函式引數做一些包裝,然後返回增加了包裝的函式,即生成了一個新函式。


讓我們看看下面這個例子:

def decorator_func(some_func):
  # define another wrapper function which modifies some_func
  def wrapper_func():
    print("Wrapper function started")
    
    some_func()
    
    print("Wrapper function ended")
    
  return wrapper_func # Wrapper function add something to the passed function and decorator returns the wrapper function
    
def say_hello():
  print ("Hello")
  
say_hello = decorator_func(say_hello)

say_hello()

# Output:
#  Wrapper function started
#  Hello
#  Wrapper function ended


上面例子中,decorator_func 就是定義的裝飾器函式,它接受some_func作為引數。它定義了一個wrapper_func函式,該函式呼叫了some_func但也增加了一些自己的程式碼。
上面程式碼中使用裝飾器的方法看起來有點複雜,其實真正的裝飾器的Python語法是這樣的:


裝飾器的Python語法

@decorator_func
def say_hi():
    print 'Hi!'


@ 符合是裝飾器的語法糖,在定義函式say_hi時使用,避免了再一次的賦值語句。
上面的語句等同於:

def say_hi():
    print 'Hi!'
say_hi = decorator_func(say_hi)


裝飾器的順序

@a
@b
@c
def foo():
    print('foo')

# 等同於:
foo = a(b(c(foo)))


帶引數函式的裝飾器

def decorator_func(some_func):
    def wrapper_func(*args, **kwargs):
        print("Wrapper function started")
        some_func(*args, **kwargs)
        print("Wrapper function ended")
    
    return wrapper_func

@decorator_func    
def say_hi(name):
    print ("Hi!" + name)


上面程式碼中,say_hi函式帶有一個引數。通常情況下,不同功能的函式可以有不同類別、不同數量的引數,在寫wrapper_func的時候,我們不確定引數的名稱和數量,可以通過*args 和 **kwargs 來引用函式引數。


帶引數的裝飾器
不僅被裝飾的函式可以帶引數,裝飾器本身也可以帶引數。參考下面的例子:

def use_logging(level):
    def decorator(func):
        def wrapper(*args, **kwargs):
            if level == "warn":
                logging.warn("%s is running" % func.__name__)
            return func(*args)
        return wrapper

    return decorator

@use_logging(level="warn")
def foo(name='foo'):
    print("i am %s" % name)


簡單來說,帶引數的裝飾器就是在沒有引數的裝飾器外面再巢狀一個引數的函式,該函式返回那個無引數裝飾器即可。


類作為裝飾器
前面我們提到裝飾器是可呼叫物件。在Python裡面,除了函式,類也是可呼叫物件。使用類裝飾器,優點是靈活性大,高內聚,封裝性。通過實現類內部的__call__方法,當使用 @ 語法糖把裝飾器附加到函式上時,就會呼叫此方法。

class Foo(object):
    def __init__(self, func):
    self._func = func

def __call__(self):
    print ('class decorator runing')
    self._func()
    print ('class decorator ending')

@Foo
def say_hi():
    print('Hi!')

say_hi()
# Output:
# class decorator running
# Hi!
# class decorator ending


functools.wraps
使用裝飾器極大地複用了程式碼,但是他有一個缺點就是原函式的元資訊不見了,比如函式的docstring、__name__、引數列表,先看看下面例子:

def decorator_func(some_func):
    def wrapper_func(*args, **kwargs):
        print("Wrapper function started")
        some_func(*args, **kwargs)
        print("Wrapper function ended")
    
    return wrapper_func

@decorator_func    
def say_hi(name):
    '''Say hi to somebody'''
    print ("Hi!" + name)

print(say_hi.__name__)  # Output: wrapper_func
print(say_hi.__doc__)   # Output: None


可以看到,say_hi函式被wrapper_func函式取代,它的__name__ 和 docstring 也自然是wrapper_func函式的了。
不過不用擔心,Python有functools.wraps,wraps本身也是一個裝飾器,它的作用就是把原函式的元資訊拷貝到裝飾器函式中,使得裝飾器函式也有和原函式一樣的元資訊。

from functools import wraps
def decorator_func(some_func):
    @wraps(func)
    def wrapper_func(*args, **kwargs):
        print("Wrapper function started")
        some_func(*args, **kwargs)
        print("Wrapper function ended")
    
    return wrapper_func

@decorator_func    
def say_hi(name):
    '''Say hi to somebody'''
    print ("Hi!" + name)

print(say_hi.__name__)  # Output: say_hi
print(say_hi.__doc__)   # Output: Say hi to somebody

 


類的內建裝飾器
類屬性@property
靜態方法@staticmethod
類方法@classmethod


通常,我們需要先例項化一個類的物件,再呼叫其方法。
若類的方法使用了@staticmethod或@classmethod,就可以不需要例項化,直接類名.方法名()來呼叫。


從使用上來看,@staticmethod不需要指代自身物件的self或指代自身類的cls引數,就跟使用普通函式一樣。@classmethod不需要self引數,但第一個引數必須是指代自身類的cls引數。如果在@staticmethod中要呼叫到這個類的一些屬性方法,只能直接類名.屬性名,或類名.方法名的方式。
而@classmethod因為持有cls引數,可以來呼叫類的屬性,類的方法,例項化物件等。

總結
通過認識Python的函式,我們逐步弄清了裝飾器的來龍去脈。裝飾器是程式碼複用的好工具,在程式設計過程中可以在適當的場景用多多使用。

相關文章