搜索
您的当前位置:首页正文

Python高级编程笔记(1)装饰器

来源:二三娱乐

装饰器

装饰器是一个用于封装函数或类的代码工具。显示地将封装器应用到函数或类上,从而它们选择加入到装饰器的功能中。能够在函数运行前进行相应的操作(确认授权,数值检查等)或运行过后清理(异常处理,输出清除等)。对于已经被装饰的函数或类本身,装饰器可以将函数注册到信号系统或者注册到Web应用程序的URI注册表中。

理解装饰器

def decorated_by(func):
    func.__doc__ += '\nDecorated by decorated_by'
    return func


def add(x, y):
    """Reutrn the sum of x and y."""
    return x + y

add = decorated_by(add)

print(help(add))

输出如下,装饰器修改了add函数的doc属性,然后返回了原来的函数对象

Help on function add in module __main__:

add(x, y)
    Reutrn the sum of x and y.
    Decorated by decorated_by

None

装饰器的语法

Python2.5为装饰器引入了特殊语法,通过在装饰器函数前放置一个@字符,并放置在被装饰的函数上

def decorated_by(func):
    func.__doc__ += '\nDecorated by decorated_by'
    return func

@decorated_by
def add(x, y):
    """Reutrn the sum of x and y."""
    return x + y

#add = decorated_by(add)

print(help(add))

装饰器应用的顺序

通过@语法我们可以使用多个装饰器,其应用顺序是按照自底向上的。

@also_decorated_by
@decorated_by
def add(x, y):
    """Reutrn the sum of x and y."""
    return x + y

首先是由解释器创建的add函数,然后应用decorated_by装饰器。该装饰器返回了一个可调用函数,该函数被发送给also_decorated_by装饰器,将结果付给赋给add函数。实际过程如下:

add = also_decorated_by(decorated_by(add))

编写装饰器的理由

  1. 可重用的功能片段
  2. 可以注册函数
  3. 显式调用,易于调试

使用时机

  1. 附加功能
  2. 数据的清理和添加
  3. 函数注册

编写装饰器

通常接受被装饰的可调用函数作为唯一参数,并且返回一个可调用函数。需要注意的是:当装饰器应用到装饰函数时(非调用装饰器时),会执行装饰代码本身。

1. 初始实例:函数注册表

registry = []

def register(decorated):
    """register 方法是一个简单的装饰器。它附加一个位置参数,该参数被装饰到注册表变量,然后返回未改变的装饰方法。"""
    registry.append(decorated)
    return decorated


@register
def foo():
    return 3

@register
def bar():
    return 5

answers = []
for func in registry:
    answers.append(func())

print(answers)
"""
answers列表中此时包含了[3, 5]。这是因为函数是按顺序执行的,所返回的值被附加到answers。
"""

设置一个registry类,将钩子添加到代码中,能够在关键时间前后执行自定义功能

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

通过几个不同的注册表实例,可以拥有完全分离的注册表。甚至可以在多个注册表中注册同一个函数

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 baz(x=7):
    return x

print(a.run_all())      # [3, 7]
print(b.run_all())      # [5, 7]

print(a.run_all(x=4))   # [4, 4]

2. 执行时封装代码

这种装饰器非常简单,因为被装饰函数是在未经过修改的条件下传递的。但是,执行被装饰方法时,可能希望运行额外的功能。为此,可以返回一个添加合适功能且(通常)在执行过程中调用被装饰方法的可调用函数。

  1. 简单的类型装饰器
def requires_ints(decorated):
    def inner(*args, **kwargs):
        kwarg_values = [i for i in kwargs.values()]

        for arg in list(args) + kwarg_values:
            if not isinstance(arg, int):
                raise TypeError('%s only accepts integers as argument.' % decorated.__name__)
        return decorated(*arg, **kwargs)
    return inner

@requires_ints
def foo(x, y):
    """Return the sum of x and y"""
    return x + y

print(help(foo))

装饰器自身是reuires_ints。它接收一个参数:decorated, 即被装饰的可调用函数。装饰器唯一做的事情就是返回一个新的可调用函数,即本地函数inner。该函数替代了被装饰方法。装饰之后,将foo赋值给了inner函数,而不是赋给原来被定义的函数。如果运行foo(3, 5), 将到利用传入的两个参数运行inner函数。inner函数执行类型检查,然后运行被装饰的方法,仅仅是由于inner函数使用返回的被封装方法(*args, **kwargs)调用它,返回值是8。如果缺少这个调用,被装饰方法将被忽略。

Help on function inner in module __main__:

inner(*args, **kwargs)

None
  1. 保存帮助信息

用一个装饰器去细究函数的文本字符串或者截取help的输出并不可行。因为装饰器是用于添加通用和可重用功能的工具,所以相对模糊的注释是有必要的。一个名为@functools.wraps的装饰器,将一个函数中的重要内部元素复制到另一个函数。

import functools

def requires_ints(decorated):
    @functools.wraps(decorated)
    def inner(*args, **kwargs):
        kwarg_values = [i for i in kwargs.values()]

        for arg in list(args) + kwarg_values:
            if not isinstance(arg, int):
                raise TypeError('%s only accepts integers as argument.' % decorated.__name__)
        return decorated(*args, **kwargs)
    return inner

增加之后,可以看到foo函数的文本字符串及其方法签名。但是从原理上来讲,仍然应用了@requires_ints装饰器,并且inner函数仍然在运行。

Help on function foo in module __main__:

foo(x, y)
    Return the sum of x and y

None
  1. 用户验证

该模式的一个常见用例是用户验证。考虑一个期望将用户作为其第一个参数的方法,首先定义两个类

class User(object):
    """A representation of a user in our application"""

    def __init__(self, username, email):
        self.username = username
        self.email = email

class AnonymousUser(User):
    """An anonymous user; a stand-in for an actual user that nonetheless is not an actual user"""
    def __init__(self):
        self.username = None
        self.email = None

    def __nonzero__(self):
        return False

我们所需的就是定义一个装饰器,用于隔离用户验证的样板代码。@requires_user装饰器能够容易地验证你得到的User对象,不是一个匿名用户。

def reuqires_user(func):
    @functools.warps(func)
    def inner(user, *args, **kwargs):
        """Verify that the user is truthy; if so, run the decorated method, and if not, raise ValueError"""
        # AnonymousUser subclass has a '__nonzero__' method that
        # return False
        if user and isinstance(user, User):
            return func(user, *args, **kwargs)
        else:
            raise ValueError("A valid user is required to run this.")
    return inner

该装饰器仅仅能够正确包装函数和静态方法,如果包装绑定到类的方法,将会失败。这是由于装饰器忽略了将self发送给绑定方法作为第一个参数的期望。

  1. 输出格式化

下为接受输出结果并将其序列化为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
def do_nothing():
    return {'status': 'done'}

print(do_nothing())

将返回的Python字典、列表、或其他对象装还未序列化JSON格式的版本

{"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):
    """Run the decorated function, serialize the result of that function to Json, and return the JSON string."""
    @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)
    return inner

@json_output
def error():
    raise JSONOutputError('this function is erratic')

print(error())

Result:

{"status": "error", "message": "this function is erratic"}

从本质上讲,装饰器是避免重复的工具,它们的部分价值就是为代码的未来维护提供钩子。装饰器主要是一个语法糖,但语法糖也有价值。

  1. 日志管理

执行时封装代码的最后一个实例是通用的日志管理函数。记录函数运行的时间到日志上

import functools
import logging
import time


def logged(method):
    """Cause the decorated method to be run and its results logged, along with some other diagnostic information"""
    @functools.wraps(method)
    def inner(*args, **kwargs):
        start =  time.time()
        return_value = method(*args, **kwargs)
        end = time.time()
        delta = end - start
        logger = logging.getLogger('decorated.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

@logged
def sleep_and_return(return_value):
    time.sleep(2)
    return return_value


print(sleep_and_return(42))

本例装饰器在后台完成工作,没有修改实际的返回结果。

Called method sleep_and_return at 1531021450.94; execution time 2.00 seconds; result 42.
42

3. 装饰器参数

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=False):
    """Run the decorated function, serialize the result of that function to JSON, and return the JSON string"""
    def actual_decorator(decorated):
        @functools.wraps(decorated)
        def inner(*args, **kwargs):
            try:
                result = decorated(*args, **kwargs)
            except JSONOutputError as ex:
                result = {
                    'satatus': 'error',
                    'message': str(ex)
                }
            return json.dumps(result, indent=indent, sort_keys=sort_keys)
        return inner
    return actual_decorator

json_output不是装饰器,而是返回装饰器的函数。它接受两个参数(indent和sort_keys),它返回另一个名为actual_decorator的函数,该函数意在用作装饰器。这是一个典型的装饰器--一个接受单独可调用函数作为参数并返回一个可调用函数(inner)的可调用函数。

@json_output(indent=4)
def do_nothing():
    return {'status':'done'}

do_nothing函数在应用装饰器语法(@)之前调用(json_output(indent=4))。因此函数调用的结果被应用到装饰器上。也就是返回的actual_decorator函数

@actual_decorator
def do_nothing():
    return {'status': 'done'}

调用do_nothing时,inner函数被调用,执行被装饰的方法,然后输出带有合适缩进的JSON结果。

  • 调用签名
@json_output
@json_output()
@json_output(indent=4)

在理想情况下装饰器可以对三种不同类型的应用程序生效,圆括号,表面该函数已调用,然后该结果应用到@。
装饰器函数只有一个,具有其他函数的所有灵活性,它可以为了响应输入而完成所需要完成的工作。

import functools
import json

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


def json_output(decorated_ = None, indent=None, sort_keys=False):
    # Did we get both a decorated method and keyword arguments?
    # that should not happen.
    if decorated_ and (indent or sort_keys):
        raise RuntimeError("Unexpected arguments")

    # Define the actual decorator function
    def actual_decorator(func):
        @functools.wraps(func)
        def inner(*args, **kwargs):
            try:
                result = func(*args, **kwargs)
            except JSONOutputError as ex:
                result = {
                    'status': 'error',
                    'message': str(ex)
                }
            return json.dumps(result, indent=indent, sort_keys=sort_keys)
        return inner

    if decorated_:
        # @json_output
        return actual_decorator(decorated_)
    else:
        # @json_output()
        return actual_decorator

三种方式测试

@json_output
def do_nothing():
    return {"status": "test"}

@json_output()
def do_nothing1():
    return {"status": "test"}

@json_output(indent=4)
def do_nothing2():
    return {"status": "test"}

结果

{"status": "test"}
{"status": "test"}
{
    "status": "test"
}

装饰类

本质上来说装饰器是一个接受可调用函数的可调用函数,并返回一个可调用函数。这意味着装饰器可以被用于装饰类和函数。类装饰器可以与被装饰类的属性交互。类装饰器可以添加属性或属性参数化,或是它可以修改一个类的API,从而使被声明的方式与实例被使用的方式不同。

import functools
import time

def sortable_by_creation_time(cls):
    original_init = cls.__init__

    @functools.wraps(original_init)
    def new_init(self, *args, **kwargs):
        original_init(self, *args, **kwargs)
        self._created = time.time()

    cls.__init__ = new_init

    cls.__lt__ = lambda self, other: self._created < other._created
    cls.__gt__ = lambda self, other: self._created > other._created

    return cls


@sortable_by_creation_time
class Sortable(object):
    def __init__(self, identifier):
        self.identifier = identifier

    def __repr__(self):
        return self.identifier

first = Sortable('first')
time.sleep(1)
second = Sortable('second')
time.sleep(1)
third = Sortable('third')

sortables = [second, first, third]
b = sorted(sortables)
print(b) # [first, second, third]

类型转换

装饰器的唯一需求是一个可调用函数接受一个可调用函数并返回一个可调用函数。没有必要返回同中类型的可调用函数。尤其是,装饰器是一个函数,但返回一个类,这个很有价值。

在python生态系统的流行任务执行器中使用的装饰器是celery。celery包提供的@celery.task装饰器期望装饰一个函数,而该装饰器实际上会返回celery内部的Task类,而被装饰的函数在子类的run方法中被使用。

class Task(object):
    """A trivial task class. Task classes have a `run` method, which runs the task."""
    def run(self, *args, **kwargs):
        raise NotImplementedError('Subclasses must implement `run`.')
    
    def identify(self):
        return 'I am a task'

def task(decorated):
    """Return a class that runs the given function if its run method is called."""
    class TaskSubclass(Task):
        def run(self, *args, **kwrags):
            return decorated(*args, **kwrags)
    return TaskSubclass

装饰器创建了Task的一个子类并返回该类。该类是一个可调用函数并调用一个类创建该类的实例,返回该类的init方法。这么做最大的价值在于为大量的扩展提供一个钩子。基本的Task类可以比run方法定义更多的内容。例如,start方法获取可以异步执行任务。基本类或许也可以提供用于保存任务状态的方法,使用一个装饰器将一个函数替换为一个类,可以使开发者只需考虑所编写任务的实际内容,而装饰器会完成剩下的工作。

  • 缺陷
@task
def foo():
    return 2 + 2

print(foo())

这个特定方法会带来一些问题,一旦任务函数被@task_class装饰器装饰时,它会变成一个类。

<__main__.task.<locals>.TaskSubclass object at 0x000001C709EB3550>

这是不好的事情,该装饰器以这样一种方式修改函数。很难接受函数被声明为foo,并以复杂了foo().run()形式执行。

修订版本:

class Task(object):
    """A trivial task class. Task classes have a `run` method, which runs the task."""
    def __call__(self, *args, **kwargs):
        return self.run(*args, **kwargs)

    def run(self, *args, **kwargs):
        raise NotImplementedError('Subclasses must implement `run`.')
    
    def identify(self):
        return 'I am a task'

def task(decorated):
    """Return a class that runs the given function if its run method is called."""
    class TaskSubclass(Task):
        def run(self, *args, **kwrags):
            return decorated(*args, **kwrags)
    return TaskSubclass()

@task
def foo():
    return 2 + 2

print(foo())  # 4
print(foo)  # <__main__.task.<locals>.TaskSubclass object at 0x000002E9832B25C0>

对代码进行了修改,第一点是用于调用基类Task而新增的call方法。第二点是@task_class装饰器现在返回TaskSubclass类的实例而不是类本身。Task新增的call方法,当foo()调用时,它的call方法被调用,该方法会调用run函数,而调用原始函数。

Top