装饰器

装饰器

装饰器只不过是一种函数,接收被装饰的可调用对象作为它的唯一参数,然后返回一个可调用对象

第一个例子:函数注册

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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章