python函式修飾器(decorator)

世有因果知因求果發表於2018-08-03

python語言本身具有豐富的功能和表達語法,其中修飾器是一個非常有用的功能。在設計模式中,decorator能夠在無需直接使用子類的方式來動態地修正一個函式,類或者類的方法的功能。當你希望在不修改函式本身的前提下擴充套件函式的功能時非常有用。

簡單地說,decorator就像一個wrapper一樣,在函式執行之前或者之後修改該函式的行為,而無需修改函式本身的程式碼,這也是修飾器名稱的來由。

關於函式

在Python中,函式是first class citizen,函式本身也是物件,這意味著我們可以對函式本身做很多有意義的操作。

將函式賦值給變數:

def greet(name):
    return "hello "+name

greet_someone = greet
print greet_someone("John")

# Outputs: hello John

函式內定義函式:

def greet(name):
    def get_message():
        return "Hello "

    result = get_message()+name
    return result

print greet("John")

# Outputs: Hello John

函式可以作為引數傳給其他函式:

def greet(name):
   return "Hello " + name 

def call_func(func):
    other_name = "John"
    return func(other_name)  

print call_func(greet)

# Outputs: Hello John

函式可以返回其他函式(函式產生函式):

def compose_greet_func():
    def get_message():
        return "Hello there!"

    return get_message

greet = compose_greet_func()
print greet()

# Outputs: Hello there!

內部函式可以訪問外部包scope(enclosing scope)

def compose_greet_func(name):
    def get_message():
        return "Hello there "+name+"!"

    return get_message

greet = compose_greet_func("John")
print greet()

# Outputs: Hello there John!

需要注意的是:這種情況下python僅僅允許"只讀"訪問外部scope的變數

開始創作我們的decorator

函式的修飾器就是已知函式的wrapper.將上述函式的好功能運用起來就能製作我們的decorator.

def get_text(name):
   return "lorem ipsum, {0} dolor sit amet".format(name)

def p_decorate(func):
   def func_wrapper(name):
       return "<p>{0}</p>".format(func(name))
   return func_wrapper

my_get_text = p_decorate(get_text)

print my_get_text("John")

# <p>Outputs lorem ipsum, John dolor sit amet</p>

這就是我們的第一個修飾器。一個函式接收另一個函式作為引數,並且產生一個新的函式,修正引數函式的功能並新增新功能,並且返回一個"generated"新函式,這樣我們後面就可以在任何地方使用這個新建立的函式了。我們也可以將修飾器函式直接賦值給引數函式名本身,這樣就覆蓋了原來的函式!

get_text = p_decorate(get_text)

print get_text("John")

# Outputs lorem ipsum, John dolor sit amet

另外一點需要注意的是:被修飾的函式get_text具有一個name引數,我們必須在wrapper函式中傳入那個引數。

python的修飾符語法糖

在上面的例子中我們通過get_text=p_decorate(get_text)的方式覆蓋了get_text從而形成了有新功能的同名函式,這個顯得有點囉嗦,python提供了簡潔清晰的對應語法。比如:

def p_decorate(func):
   def func_wrapper(name):
       return "<p>{0}</p>".format(func(name))
   return func_wrapper

@p_decorate
def get_text(name):
   return "lorem ipsum, {0} dolor sit amet".format(name)

print get_text("John")

# Outputs <p>lorem ipsum, John dolor sit amet</p>

在上面的例子程式碼中,@符號後面的是修飾器本身,緊跟後面的則是將被修飾的函式(將隱含著賦值覆蓋操作)。這種語法等價於使用@後面的修飾器先對get_text修飾,並且返回產生的新函式替代被修飾的函式名。後面直接用被修飾的函式名呼叫,但是卻有了新的功能!

現在,我們希望再新增兩個其他的函式來修飾get_text分別再增加一個div和strong tag

def p_decorate(func):
   def func_wrapper(name):
       return "<p>{0}</p>".format(func(name))
   return func_wrapper

def strong_decorate(func):
    def func_wrapper(name):
        return "<strong>{0}</strong>".format(func(name))
    return func_wrapper

def div_decorate(func):
    def func_wrapper(name):
        return "<div>{0}</div>".format(func(name))
    return func_wrapper

# 基礎用法:
get_text = div_decorate(p_decorate(strong_decorate(get_text)))
#等價於:
@div_decorate
@p_decorate
@strong_decorate
def get_text(name):
   return "lorem ipsum, {0} dolor sit amet".format(name)

print get_text("John")

# Outputs <div><p><strong>lorem ipsum, John dolor sit amet</strong></p></div>

需要注意的是修飾器的順序是有關係的。如果順序不同,則結果也不同。

method修飾

python中類的方法是一個首引數為self指標的函式。我們可以和普通函式一樣去做修飾,但是需要注意的是必須在wrapper函式中考慮self指標引數。

def p_decorate(func):
   def func_wrapper(self):
       return "<p>{0}</p>".format(func(self))
   return func_wrapper

class Person(object):
    def __init__(self):
        self.name = "John"
        self.family = "Doe"

    @p_decorate
    def get_fullname(self):
        return self.name+" "+self.family

my_person = Person()
print my_person.get_fullname()

一個更好的方案是調整程式碼使得我們的修飾器對於函式或者method同樣適用。這可以通過通過將args和*kwargs放到wrapper函式中作為引數來實現,這樣可以接受任意個數的引數或者keyword型引數。

def p_decorate(func):
   def func_wrapper(*args, **kwargs):
       return "<p>{0}</p>".format(func(*args, **kwargs))
   return func_wrapper

class Person(object):
    def __init__(self):
        self.name = "John"
        self.family = "Doe"

    @p_decorate
    def get_fullname(self):
        return self.name+" "+self.family

my_person = Person()

print my_person.get_fullname()

 向decorator傳入引數

def tags(tag_name):
    def tags_decorator(func):
        def func_wrapper(name):
            return "<{0}>{1}</{0}>".format(tag_name, func(name))
        return func_wrapper
    return tags_decorator

@tags("p")
def get_text(name):
    return "Hello "+name

print get_text("John")

# Outputs <p>Hello John</p>

在這個例子中,貌似又更加複雜了一點,但是帶來了更多的靈活性。decorator必須僅接受一個被修飾的函式為引數,這也是為什麼我們必須再外包裹一層從而接受那些額外的引數並且產生我們的decorator的原因。這個例子中tags函式是我們的decorator generator

除錯decorated function

從上面的描述可知,decorators負責包裹被修飾的函式,這帶來一個問題就是如果要除錯程式碼可能有問題,因為wrapper函式並不會攜帶原函式的函式名,模組名和docstring等資訊,比如基於以上的例子,如果我們列印get_text.__name__則返回func_wrapper而不是get_text,原因就是__name__,__doc__,__module__這些屬性都被wrapper函式所(func_wrapper)過載。雖然我們可以手工重置(在func_wrapper),但是python提供了更好的辦法:

functools

functools模組包含了wraps函式。wraps也是一個decorator,但是僅僅用於更新wrapping function(func_wrapper)的屬性為原始函式的屬性(get_text),看下面的程式碼:

from functools import wraps

def tags(tag_name):
    def tags_decorator(func):
        @wraps(func)
        def func_wrapper(name):
            return "<{0}>{1}</{0}>".format(tag_name, func(name))
        return func_wrapper
    return tags_decorator

@tags("p")
def get_text(name):
    """returns some text"""
    return "Hello "+name

print get_text.__name__ # get_text
print get_text.__doc__ # returns some text
print get_text.__module__ # __main__

何時使用decorator?

在上面的例子中僅僅羅列了修飾器的基礎用法,實際上這個機制是非常強大有用的,總的來說,decorator在你希望在不修改函式本身程式碼的前提下擴充套件函式的功能時非常有用。

一個經典的例子timeout修飾函式:

https://wiki.python.org/moin/PythonDecoratorLibrary#Function_Timeout

timeout修飾符產生函式的定義:

import signal
import functools

class TimeoutError(Exception): pass

def timeout(seconds, error_message = 'Function call timed out'):
    def decorated(func):
        def _handle_timeout(signum, frame):
            raise TimeoutError(error_message)

        def wrapper(*args, **kwargs):
            signal.signal(signal.SIGALRM, _handle_timeout)
            signal.alarm(seconds)
            try:
                result = func(*args, **kwargs)
            finally:
                signal.alarm(0)
            return result

        return functools.wraps(func)(wrapper)

    return decorated

使用:

import time

@timeout(1, 'Function slow; aborted')
def slow_function():
    time.sleep(5)

 

https://www.thecodeship.com/patterns/guide-to-python-function-decorators/

https://wiki.python.org/moin/PythonDecoratorLibrary

 

 

 

相關文章