已复制
全屏展示
复制代码

深入浅出 Python 装饰器精通


· 8 min read

什么是装饰器

装饰器,顾名思义就是用来做装饰用的,Python中装饰器用来装饰函数,它的好处是不需要对原来的函数做任何修改就可以对函数进行装饰。

装饰器的实现

现在要获取函数 a() 的运行时间,可以编写一个装饰器来装饰,下面分三个步骤,一步一步引出装饰器的语法糖 @ 。

把函数传递到一个新函数获取

import time

def a():
    time.sleep(1)
    print('in a()')

def timeit(func):
    def wrapper():
        start = int(time.time())
        func()
        end =int(time.time())
        print('used:{} second(s)'.format(end - start))
    return wrapper

timeit(a)()

把函数作为参数传递到timeit(),在timeit()里面返回wrapper()函数,这个wrapper()的作用就是获取函数a()的运行时间。 在timeit(a)()中, timeit(a)获得的是一个函数,最后面的括号是函数的调用。

这种方法的缺点就是是改变了函数的调用方式,如果函数 a() 在很多地方都有,那么你需要在每个地方都修改一下调用方式才能获取到函数的运行时间。

更改函数的调用方式

要解决上面的问题,我们可以将 timeit(a) 重新赋值给 a , 这样就可以直接使用 a() 这样的方式来获取函数运行时间了。

import time

def a():
    time.sleep(1)
    print('in a()')

def timeit(func):
    def wrapper():
        start = int(time.time())
        func()
        end =int(time.time())
        print('used:{} second(s)'.format(end - start))
    return wrapper

a = timeit(a)
a()

使用Python提供的语法糖 @

为了简洁至上,Python 已经帮我们用语法糖 @ 来实现了上面的赋值(a = timeit(a))那个步骤。

import time

def timeit(func):
    def wrapper():
        start = int(time.time())
        func()
        end =int(time.time())
        print('used:{} second(s)'.format(end - start))
    return wrapper

@timeit
def a():
    time.sleep(1)
    print('in a()')

a()

使用@timeit,在函数的定义处加上这一行,与另外写 a = timeit(a)完全等价,千万不要以为@有另外的魔力。除了字符输入少了一些,还有一个额外的好处就是这样看上去更有装饰器的感觉。

双重装饰器

有时一个函数可能需要不止一个装饰器,多个装饰器要注意装饰器的顺序。

def makebold(fn):
    def wrapper():
        return "<b>" + fn() + "</b>"
    return wrapper

def makeitalic(fn):
    def wrapper():
        return "<i>" + fn() + "</i>"
    return wrapper

@makebold
@makeitalic
def say():
    return("hello")

print(say())

@makeitalic装饰器的作用是把数据变斜体, @makebold 的作用是把数据变为粗体。你可以把它们两个调换一下位置看看有什么效果。

保留函数元信息

当写了一个装饰器作用在某个函数上时,如果不做任何操作,这个函数的重要的元信息比如名字、文档字符串、注解和参数签名都丢失了。任何时候定义装饰器的时候,都应该使用 functools 库中的 @wraps 装饰器来注解底层包装函数。

import time
from functools import wraps

def timethis(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        func(*args, **kwargs)
        end = time.time()
        print(func.__name__, end-start)
    return wrapper

@timethis
def myfun():
    '''
    myfun's doc!
    '''
    print("original function!")
    time.sleep(2)

myfun()
print(myfun.__name__)
print(myfun.__doc__)

解除一个装饰器

当想撤销已装饰的函数时,可以通过访问 __wrapped__ 属性来访问原始的未包装的那个函数。

import time
from functools import wraps

def timethis(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        func(*args, **kwargs)
        end = time.time()
        print(func.__name__, end-start)
    return wrapper

@timethis
def myfun():
    '''
    myfun's doc!
    '''
    print("original function!")
    time.sleep(2)

myfun = myfun.__wrapped__
myfun()
  • 这里的方法仅仅适用于在包装器中正确使用了 @wraps 的情况。
  • 如果有多个装饰器,那么访问 __wrapped__ 属性的行为是不可预知的,应该避免这种行为。
  • 内置的装饰器 @staticmethod@classmethod 无法使用 __wrapped__ 属性,因为这两个装饰器没有使用 @wraps 装饰。

有参数的装饰器

当装饰器有参数时,比如下面的 delay 装饰器。

import time

def delay(seconds):
    def new_deco(func):
        time.sleep(seconds)
        print(func)
        return func
    return new_deco

@delay(2)  
def func_one():
    print("func_one() , do something!")

func_one()

第一个函数 delay 是装饰函数,它的参数是用来 加强装饰 的。由于此函数并非被装饰的函数对象,所以在内部必须至少创建一个接受被装饰函数的函数,然后返回这个对象。实际上此时相当于 func_one = delay(2)(func_one)

带可选参数的装饰器

一个装饰器,有时不需要传递参数(@logged),有时需要传递参数(@logged(level=logging.DEBUG))。为了实现这种这种调用方式,装饰器参数可以使用 带默认值的位置参数 来实现。

from functools import wraps, partial
import logging

def logged(func=None, level='DEBUG'):
    if func is None:
        return partial(logged, level=level)

    @wraps(func)
    def wrapper(*args, **kwargs):
        print('log level: {0}'.format(level))
        return func(*args, **kwargs)
    return wrapper

@logged
def add(x, y):
    return x + y

@logged(level='INFO')
def plus(x, y):
    return x + y

print(add(3,4))
print(plus(3,4))

结果如下,可以看到,@logged 装饰器可以同时不带参数(注意调用时没有括号)或带参数,这样做仅仅是一种编程习惯。

log level: DEBUG
7
log level: INFO
7

对于无参数的装饰器:

@logged
def add(x, y):
    return x + y
    

等价于

def add(x, y):
    return x + y
add = logged(add)

这时候,被装饰函数会被当做第一个参数直接传递给 logged 装饰器。 因此,logged() 中的第一个参数就是被包装函数本身。所有其他参数都必须有默认值。

装饰器实现函数参数类型检查

在函数定义处使用装饰器 @typeassert 即可对函数的参数做类型限制,看下面的例子。

from inspect import signature
from functools import wraps

def typeassert(*ty_args, **ty_kwargs):
    def decorate(func):
        # If in optimized mode, disable type checking
        if not __debug__:
            return func

        # Map function argument names to supplied types
        sig = signature(func)
        bound_types = sig.bind_partial(*ty_args, **ty_kwargs).arguments

        @wraps(func)
        def wrapper(*args, **kwargs):
            bound_values = sig.bind(*args, **kwargs)
            # Enforce type assertions across supplied arguments
            for name, value in bound_values.arguments.items():
                if name in bound_types:
                    if not isinstance(value, bound_types[name]):
                        raise TypeError(
                            'Argument {} must be {}'.format(name, bound_types[name])
                            )
            return func(*args, **kwargs)
        return wrapper
    return decorate

@typeassert(int, list, z=dict)
def spam(x, y, z={'age':20}):
    print(x, y, z)

spam(1, 2, 3)
spam(1, [2,3], {'age':23})

内置装饰器

内置的装饰器中常用的有三个,分别是staticmethod、classmethod、property,作用分别是把类中定义的实例方法变成静态方法、类方法、类属性。这几个内置装饰器在  Python 面向对象详解  一文中有详细讲解。

将装饰器定义在类内部

将我们定义的一些装饰器进行分类管理,将功能相似的装饰器放在一个类里面。 定义是需要注意的是:装饰器是作为一个实例方法还是类方法。

from functools import wraps

class Dec:
    def decorator1(self, func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print('Decorator 1')
            return func(*args, **kwargs)
        return wrapper

    @classmethod
    def decorator2(cls, func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print('Decorator 2')
            return func(*args, **kwargs)
        return wrapper

d = Dec()
@d.decorator1
def foo():
    pass

@Dec.decorator2
def bar():
    pass

foo()
bar()

上面的例子中,一个是实例调用,一个是类调用。在类中定义装饰器这种方法,在标准库中有很多这样的例子。 比如 @property 装饰器实际上是一个类,它里面定义了三个方法 getter(), setter(), deleter(), 每一个方法都是一个装饰器。

为类方法和静态方法提供装饰器

当给类方法或静态方法提供装饰器时。要确保装饰器在 @classmethod@staticmethod 之前。例如:

import time
from functools import wraps

def timethis(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        r = func(*args, **kwargs)
        end = time.time()
        print(end-start)
        return r
    return wrapper


class Spam:
    @timethis
    def instance_method(self, n):
        while n > 0:
            print(self, n)
            n -= 1

    @classmethod
    @timethis
    def class_method(cls, n):
        while n > 0:
            print(cls, n)
            n -= 1

    @staticmethod
    @timethis
    def static_method(n):
        while n > 0:
            print(n)
            n -= 1

s = Spam()
s.instance_method(3)
print()
Spam.class_method(3)
print()
Spam.static_method(3)

运行结果:

<__main__.Spam object at 0x7fc498657d68> 3
<__main__.Spam object at 0x7fc498657d68> 2
<__main__.Spam object at 0x7fc498657d68> 1
6.079673767089844e-05

<class '__main__.Spam'> 3
<class '__main__.Spam'> 2
<class '__main__.Spam'> 1
1.1444091796875e-05

3
2
1
7.62939453125e-06

类装饰器

类装饰器通常可以作为其他高级技术比如混入或元类的一种非常简洁的替代方案。

def log_getattribute(cls):
    ori_getattribute = cls.__getattribute__

    def new_getattribute(self, name):
        print('getting:', name)
        return ori_getattribute(self, name)

    cls.__getattribute__ = new_getattribute
    return cls

@log_getattribute
class A:
    def __init__(self,x):
        self.x = x
    def spam(self):
        print(self.x)

a = A(3)
a.spam()

上面的类装饰器可以用继承的方式实现


class LoggedGetattribute:
    def __getattribute__(self, name):
        print('getting:', name)
        return super().__getattribute__(name)

class A(LoggedGetattribute):
    def __init__(self,x):
        self.x = x
    def spam(self):
        print(self.x)

a = A(3)
a.spam()

某种程度上来讲,类装饰器方案显得更加直观,并且它不会引入新的继承体系。它的运行速度也更快一些, 因为他并不依赖 super() 函数。

🔗

文章推荐