裝飾器

裝飾器

裝飾器只不過是一種函數,接收被裝飾的可調用對象作爲它的唯一參數,然後返回一個可調用對象

第一個例子:函數註冊

registry = []
# 被裝飾的對象,是一個可調用對象
def register(decorated):
    registry.append(decorated)
    return decorated

註冊器方法是一個簡單的裝飾器,會把被裝飾函數添加到registry中,然後不做改變的返回裝飾方法

@register
def foo():
    return 3

現在對上面的註冊函數的方法進行封裝

class Registry(object):
    def __init__(self):
        self._functions = []

    def register(self, decorated):
        self._functions.append(decorated)
   		return decorated
    
    def run_all(self, *args, **kwargs):
        return_values = []
        for func in self._functions:
            return_values.append(func(*args, **kwargs))
        return return_values

通過上面的類,我們可以實現一些完全分開的註冊器,使用相同的實例方法 register,但是可以給不同的對象實現註冊器的功能,見下面

a = Registry()
b = Registry()

@a.register
def foo(x=3):
    return x

@b.register
def bar(x=5):
    return x

@a.register
@b.register
def boll(x=7):
    return x

# 運行兩個註冊器的run_alll方法,得到如下結果:
a.run_all() # [3, 7]
b.run_all() # [5, 7]

接下來講解一下@functools.wraps(decorated)

正常情況下,被裝飾器裝飾後的函數,本身的_name_ 和 __doc__會發生變化,這個方法消除這些副作用的,它能保留原有函數的名稱和doc屬性

第二個例子 簡單的類型檢查

import functools

def requires_ints(decorated):
    @functools.wraps(decorated)
    def inner(*args, **kwargs):
        kwargs_values = [for i in kwargs.values()]
        params = args.extend(kwargs_values)
        for param in params:
            if not isinstance(param, int):
                raise TypeError('%s only accepts integers as arguments.' 			                     %decorated.__name__)
            return decorated(*args, **kwargs)
        return inner

被裝飾的函數

@requires_ints
def foo(x, y):
    return x + y 

# 現在當你運行help(foo)的結果:
Help on function foo in module __main__:
foo(x, y)
Return the sum of x and y.
(END)

# 可以發現函數的__name__ 和 __doc__ 屬性並沒有發生變化

第三個例子 用戶認證

class User(object):
    def __init__(self, username, email):
        self.username = username
        self.email = email 
        
class AnonymousUser(User):
    """匿名用戶
    """
    def __init__(self):
        self.username = None
        self.email = None
        
    def __nonzero__(self):
        return False
       

裝飾器在此成爲隔離用戶驗證的有力工具。@requires_user裝飾器可以很輕鬆地認證你獲得了一個User對象並且不是匿名user

import functools
def requires_user(func):
	@functools.wraps(func):
    def inner(user, *args, **kwargs):
        if user and isinstance(user, User):
            return func(*args, **kwargs)
        else:
			raise ValueError('A valid user is required to run this.')
	return inner  

第三個例子 格式化輸出

除了過濾一個函數的輸入,裝飾器的另一個用處是過濾一個函數的輸出。當你用Python工作時,只要可能就希望使用Python本地對象。然而通常想要一個序列化的輸出格式(例如,JSON)

import functools
import json

def json_output(decorated):
	"""Run the decorated function, serialize the result of 
     that function to JSON, and return the JSON string.
 	"""
 	@functools.wraps(decorated)
 	def inner(*args, **kwargs):
  		result = decorated(*args, **kwargs)
  		return json.dumps(result)
 	return inner

給一個 簡單函數應用@json_output 裝飾器 :

@json_output
def do_noting():
    return {'status': 'done'}

然而,由於應用的需求會擴展,還是考慮一下擁有此裝飾器的價值。

例如,某種異常需要被捕獲,並以特定的格式化json輸出,而不是讓異常上浮產生堆棧跟蹤,該怎麼做?因爲有裝飾器,這個功能很容易添加。

import functools 
import json

class JsonOutputError(Exception):
    def __init__(self, message):
        self.message = message
        
	def __str__(self):
        return self.message
    
def json_output(decorated):
    @functools.wraps(decorated)
    def inner(*args, **kwargs):
        try:
            result = decorated(*args, **kwargs)
        except JsonOutputError as ex:
            result = {
                'status': 'error',
                'message': 'str(ex)'
            }
        	return result
        return inner
        

現在,如果一個用@json_output裝飾的函數拋出了JsonOutputError異常,就會有特別的錯誤處理:

@json_output
def error():
    raise JsonOutputError('This function is erratic.')

運行error函數

>>> error()
'{"status": "error", "message": "This function is erratic."}'

第四個例子 日誌記錄Logging

import functools
import logging
import time

def logged(method):
 	@functools.wraps(method)
 	def inner(*args, **kwargs): 
  	start = time.time()
  	return_value = method(*args, **kwargs)
  	end = time.time()
  	delta = end - start
  	logger = logging.getLogger('decorator.logged')
  	logger.warn('Called method %s at %.02f; execution time %.02f seconds; result %r.' %
   	(method.__name__, start, delta, return_value))    
  	return return_value
return inner

帶參數的裝飾器

就是在不帶參數的裝飾器上面再加一層包裝,是這個函數變成一個裝飾器函數,主要用來返回一個裝飾器

import functools 
import json

class JsonOutputError(Exception):
    def __init__(self, message):
        self.message = message
        
	def __str__(self):
        return self.message

def json_output(indent=None, sort_keys=None)
    def actual_decorator(decorated):
        @functools.wraps(decorated)
        def inner(*args, **kwargs):
            try:
                result = decorated(*args, **kwargs)
            except JsonOutputError as ex:
                result = {
                    'status': 'error',
                    'message': 'str(ex)'
                }
            return json.dumps(result, indent=indent, sort_keys=sort_keys)
         return inner
	return actual_decorator

裝飾器中的那些坑

裝飾器可以讓你代碼更加優雅,減少重複,但也不全是優點,也會帶來一些問題。

1.位置錯誤的代碼

讓我們直接看實例代碼

def html_tags(tag_name):
    print 'begin outer function.'
    def wrapper_(func):
        print "begin of inner wrapper function."
        def wrapper(*args, **kwargs):
            content = func(*args, **kwargs)
            print "<{tag}>{content}</{tag}>".format(tag=tag_name, content=content)
        print 'end of inner wrapper function.'
        return wrapper
    print 'end of outer function'
    return wrapper_

@html_tags('b')
def hello(name='Toby'):
    return 'Hello {}!'.format(name)

hello()
hello()

在裝飾器中我在各個可能的位置都加上了print語句,用於記錄被調用的情況。你知道他們最後打印出來的順序嗎?如果你心裏沒底,那麼最好不要在裝飾器函數之外添加邏輯功能,否則這個裝飾器就不受你控制了。以下是輸出結果:

begin outer function.
end of outer function
begin of inner wrapper function.
end of inner wrapper function.
<b>Hello Toby!</b>
<b>Hello Toby!</b>

2.錯誤的函數簽名和文檔

裝飾器裝飾過的函數看上去名字沒變,其實已經變了。

def logging(func):
    def wrapper(*args, **kwargs):
        """print log before a function."""
        print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__)
        return func(*args, **kwargs)
    return wrapper

@logging
def say(something):
    """say something"""
    print "say {}!".format(something)

print say.__name__  # wrapper

可以在wrapper上加上functools.wraps解決

3.不能裝飾@staticmethod 或者 @classmethod

當你想把裝飾器用在一個靜態方法或者類方法時,不好意思,報錯了。

class Car(object):
    def __init__(self, model):
        self.model = model

    @logging  # 裝飾實例方法,OK
    def run(self):
        print "{} is running!".format(self.model)

    @logging  # 裝飾靜態方法,Failed
    @staticmethod
    def check_model_for(obj):
        if isinstance(obj, Car):
            print "The model of your car is {}".format(obj.model)
        else:
            print "{} is not a car!".format(obj)

"""
Traceback (most recent call last):
...
  File "example_4.py", line 10, in logging
    @wraps(func)
  File "C:\Python27\lib\functools.py", line 33, in update_wrapper
    setattr(wrapper, attr, getattr(wrapped, attr))
AttributeError: 'staticmethod' object has no attribute '__module__'
"""

前面已經解釋了@staticmethod這個裝飾器,其實它返回的並不是一個callable對象,而是一個staticmethod對象,那麼它是不符合裝飾器要求的(比如傳入一個callable對象),你自然不能在它之上再加別的裝飾器。要解決這個問題很簡單,只要把你的裝飾器放在@staticmethod之前就好了,因爲你的裝飾器返回的還是一個正常的函數,然後再加上一個@staticmethod是不會出問題的。

class Car(object):
    def __init__(self, model):
        self.model = model

    @staticmethod
    @logging  # 在@staticmethod之前裝飾,OK
    def check_model_for(obj):
        pass

如何優化你的裝飾器

decorator.py

decorator.py 是一個非常簡單的裝飾器加強包。你可以很直觀的先定義包裝函數wrapper(),再使用decorate(func, wrapper)方法就可以完成一個裝飾器。

from decorator import decorate

def wrapper(func, *args, **kwargs):
    """print log before a function."""
    print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__)
    return func(*args, **kwargs)

def logging(func):
    return decorate(func, wrapper)  # 用wrapper裝飾func

你也可以使用它自帶的@decorator裝飾器來完成你的裝飾器。

from decorator import decorator

@decorator
def logging(func, *args, **kwargs):
    print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__)
    return func(*args, **kwargs)

decorator.py實現的裝飾器能完整保留原函數的namedocargs,唯一有問題的就是inspect.getsource(func)返回的還是裝飾器的源代碼,你需要改成inspect.getsource(func.__wrapped__)

wrapt

wrapt是一個功能非常完善的包,用於實現各種你想到或者你沒想到的裝飾器。使用wrapt實現的裝飾器你不需要擔心之前inspect中遇到的所有問題,因爲它都幫你處理了,甚至inspect.getsource(func)也準確無誤。

import wrapt

# without argument in decorator
@wrapt.decorator
def logging(wrapped, instance, args, kwargs):  # instance is must
    print "[DEBUG]: enter {}()".format(wrapped.__name__)
    return wrapped(*args, **kwargs)

@logging
def say(something): pass

使用wrapt你只需要定義一個裝飾器函數,但是函數簽名是固定的,必須是(wrapped, instance, args, kwargs),注意第二個參數instance是必須的,就算你不用它。當裝飾器裝飾在不同位置時它將得到不同的值,比如裝飾在類實例方法時你可以拿到這個類實例。根據instance的值你能夠更加靈活的調整你的裝飾器。另外,argskwargs也是固定的,注意前面沒有星號。在裝飾器內部調用原函數時才帶星號。

如果你需要使用wrapt寫一個帶參數的裝飾器,可以這樣寫。

def logging(level):
    @wrapt.decorator
    def wrapper(wrapped, instance, args, kwargs):
        print "[{}]: enter {}()".format(level, wrapped.__name__)
        return wrapped(*args, **kwargs)
    return wrapper

@logging(level="INFO")
def do(work): pass
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章