Python 装饰器decorator 圣经

本文围绕Python装饰器展开,介绍了普通装饰器的原理、作用、语法糖、参数等内容,还讲解了类装饰器,包括可成为装饰器的类和可装饰类的装饰器。此外,说明了如何将装饰器放到类里,最后详细解释了@wraps装饰器消除副作用的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

普通装饰器decorator

0. 万能公式,非常重要

  • 在一个函数上添加装饰器,就等价于,这个函数名=装饰器调用这个函数

1. 便于入门的decorator原理

  • decorator是一个输入和输出都是函数的函数。请暂时这样理解,以便入门。

2. 理解函数

  • 在Python中定义一个函数,其实就是新建了一个变量,这个变量里保存了函数对象

    def double(x):
        return x * 2
    print(double)  # <function double at 0x10688a710>
    
  • 函数对象有一个特点就是callable,在Python中callable这个东西后面是可以跟一对小括号的,从而调用它。

    print(double(2))  # 4
    
  • 实际上在Python的语法层面,任何的东西都可以调用,只不过如果这个东西不是callable的话,会在Python的runtime时会出错。

    import dis
    # 例如我们让1进行调用,那么也是可以通过编译的,只不过会给出syntax warning,但是它的字节码表示一样可一LOAD完这个1之后再尝试call它
    dis.dis("1()")
    
  • 当我们理解了函数在Python中只是一个普通的对象后,我们就可以理解函数可以被当作参数传进其他的函数里。

    def double(x):
        return x * 2
    def triple(x):
        return x * 3
    def calc_number(func, x):
        print(func(x))
    
    calc_number(double, 3)  # 6
    calc_number(triple, 3)  # 9
    
  • 函数不单单可以作为变量传进其他函数,函数本身也可以成为一个返回值。

    def get_multiple_func(n):
        def multiple(x):
            # multiple函数return什么值是由get_multiple_func的n决定的
            return n * x
        return multiple
    
    double = get_multiple_func(2)
    triple = get_multiple_func(3)
    
    print(double(2))  # 4
    print(triple(3))  # 9
    

    此时我们就理解了“函数的返回值可以是一个函数”,那么decorator相对来说就容易理解了。

3. 装饰器的作用:

在不改变原有功能代码的基础上,添加额外的功能,如用户验证等。

4. 装饰器的语法糖

@ 符号是装饰器的语法糖。它放在一个函数开始定义的地方(头顶),和这个函数绑定在一起。

在我们调用这个函数的时候,会先将这个函数做为参数传入它头顶,即装饰器里。

5. 装饰器顺序

一个函数可以同时定义多个装饰器,比如:

@a
@b
@c
def f ():
    pass

它的执行顺序是从里到外,最先调用最里层的装饰器,最后调用最外层的装饰器,它等效于:

f = a(b(c(f)))

6. 极简的装饰器

  • decorator本身就是一个callable,它没有特殊的地方,那么我们可以暂时理解为decorator本身就是一个函数,@后面跟的这个名字就是函数名,我们定义了一个函数叫做dec
def dec(f):
    pass

@dec
def double(x):
    return x * 2

# 第4行到第6行的代码完全等价于:double = dec(double)
  • decorator是一个输入和输出都是函数的函数。当然了输入一定是函数,但是输出不一定是,下面是极端的例子,没有人会这样写代码。

    def dec(f):
        return 1
    
    @dec
    def double(x):
        return x * 2
    
    print(double)  # 1
    

    注意,我们print的是double本身,而不是对double进行调用。

    double = dec(double)
    # dec(double)返回的是1
    # 1被赋值给double变量
    # print(double)就是1
    
  • 上面的例子只是极端情况,我们还是要认为decorator就是输入和输出都是函数的函数。

7. 装饰器的参数

无参 函数装饰器

import time

def timeit(f):
    def wrapper(x):
        start = time.time()
        ret = f(x)
        print(time.time() - start)
        return ret
    return wrapper

# 等价于:my_func = timeit(my_func)
@timeit
def my_func(x):
    time.sleep(x)

my_func(1) # 等价于:timeit(my_func)(1)
# 这是通用写法,用可变参数来适配各种被包装的函数
import time


def timer(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        func(*args, **kwargs)  # 这是函数真正执行的地方
        stop_time = time.time()
        cost_time = stop_time - start_time
        print("花费时间:{}秒".format(cost_time))

    return wrapper


@timer
def want_sleep(sleep_time):
    time.sleep(sleep_time)


want_sleep(3)

有参 函数装饰器

import time

def timeit(iteration):  # iteration是运行多少次。返回的是inner函数,相当于是之前没有参数的decorator
    def inner(f):
        def wrapper(*args, **kwargs): # 里面运行了iteration次的函数f
            start = time.time()
            for _ in range(iteration):
                ret = f(*args, **kwargs)
            print(time.time() - start)
            return ret
        return wrapper
    return inner

# 等价于:double = timeit(10)(double)
# 即:
# inner = timeit(10)
# double = inner(double)
@timeit(10)
def double(x):
    return x * 2

double(1) # 等价于:timeit(10)(double)(1)
import time


def timer(sleep_time):
    def outter(func):
        def wrapper(*args, **kwargs):
            start_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())
            print("start_time:" + str(start_time))
            result = func(*args, **kwargs)  # 这是函数真正执行的地方
            time.sleep(sleep_time)
            stop_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())
            print("stop_time:" + str(stop_time))
            return result

        return wrapper

    return outter


@timer(sleep_time=3)
def sleep_status(name):
    print(name + " is sleeping...")
    return name + " is wake..."


r = sleep_status("zhangsan")
print(r)

# 输出
# start_time:2022-09-23 23:53:16
# zhangsan is sleeping...
# stop_time:2022-09-23 23:53:19
# zhangsan is wake...

类装饰器class decorator

  • 类装饰器class decorator这个名称实际上是有一定的歧义的。

    • 在有些地方是指:“可以成为装饰器的类”。这说的是装饰器本身。用中文的语境来描述,应该是“装饰器类”,而不是“类装饰器”。
    • 在有些地方是指:“可以装饰类的装饰器”。这说的是装饰器要装饰的对象。用中文的语境来描述,应该是“类 的 装饰器”。
  • 装饰器本身既可以是函数,还可以是类。而装饰的对象同样既可以是函数也可以是类。

0. 万能公式,非常重要

  • 万能公式非常重要,只要你把它转换成等价形式,decorator就比较好读,而转换成等价形式之后,我们就知道代码如何写,它的输入应该是什么,它的输出应该是什么。

1. 可以成为装饰器的类

装饰器类 的实现,必须实现__call____init__两个内置函数。

当使用 @ 形式将装饰器附加到函数上时,就会调用此方法。

无参 装饰器类

__init__:接收被装饰函数
__call__:实现装饰逻辑。

import time

class Timer:
    def __init__(self, func):
        # 函数add被传入__init__方法里,所以add被作为参数保存在了self.func里
        self.func = func
    # __call__魔术方法的意思,让所有这个类的实例都变成一个callable
    # 简单理解就是这个类的实例,都可以当作函数用,你都可以调用它 即在后面用小括号
    def __call__(self, *args, **kwargs):
        start = time.time()
        ret = self.func(*args, **kwargs)  # self.func就是原来的add函数
        print(f"Time: {time.time() - start}")
        return ret

# 把Timer这个类本身当作一个装饰器,装饰在了add函数上
# 等价于:add = Timer(add)
# @Timer装饰器相当于把名为add的变量,其值从一个函数变成了一个Timer类的实例
@Timer
def add(a, b):
    return a + b

print(type(add))  # <class '__main__.Timer'>
print(add(2, 3))  # 实际上是调用了Timer类的实例add的__call__方法,此时2和3参数就被传到__call__的参数里。
"""
Time: 5.7220458984375e-06
5
"""
  • 通过上面的例子,我们可以看到和前面讲的函数decorator其实没有很大的区别。原来的函数装饰器是“函数调用一个函数返回一个函数”,更确切的说是“函数调用一个函数返回一个callable”。
  • 现在是类调用一个函数返回一个实例,这个实例依然是callable
  • 所以我们只需要把decorator在脑海中进行等价转换就非常容易理解了。
class Logger(object):
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print("[INFO]: the function {func}() is running..." \
              .format(func=self.func.__name__))
        return self.func(*args, **kwargs)


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


say("hello")

# 输出
# [INFO]: the function say() is running...
# say hello!

有参 装饰器类

带参数和不带参数的类装饰器有很大的不同。

__init__ :不再接收被装饰函数,而是接收传入参数。
__call__:接收被装饰函数,实现装饰逻辑。

import time

class Timer:
    def __init__(self, prefix):
        self.prefix = prefix
    def __call__(self, func):
        def wrapper(*args, **kwargs):
            start = time.time()
            ret = func(*args, **kwargs)
            print(f"{self.prefix} {time.time() - start}")
            return ret
        return wrapper

# 等价于:add = Timer(prefix="curr_time: ")(add)
# 那么此时add就是wrapper了
@Timer(prefix="curr_time: ")
def add(a, b):
    return a + b

print(add(2, 3))
"""
curr_time:  1.9073486328125e-06
5
"""
class Logger(object):
    def __init__(self, level='INFO'):
        self.level = level

    def __call__(self, func):  # 接受函数
        def wrapper(*args, **kwargs):
            print("[{level}]: the function {func}() is running..." \
                  .format(level=self.level, func=func.__name__))
            func(*args, **kwargs)

        return wrapper  # 返回函数


@Logger(level='WARNING')
def say(something):
    print("say {}!".format(something))


say("hello")

# 输出
# [WARNING]: the function say() is running...
# say hello!

2. 可以装饰类的装饰器

无参 可以装饰类的装饰器

  • 在Python里,自定义的class类的实例,如果我们直接打印实例,是不会打印出任何有价值的信息的,只会打印出这个实例对象是一个什么类。

    class MyObject:
        def __init__(self, a, b):
            self.a = a
            self.b = b
    
    o = MyObject(1, 2)
    print(o)  # <__main__.MyObject object at 0x109447fd0>
    
  • 那么我们可以通过重载其__str__方法,来改变print出来的结果

    class MyObject:
        def __init__(self, a, b):
            self.a = a
            self.b = b
        def __str__(self):
            return str(self.__dict__)
    
    o = MyObject(1, 2)
    print(o)  # {'a': 1, 'b': 2}
    
  • 当然,如果每一个对象都改的话也挺麻烦的。此时“装饰类的装饰器”就派上了用场。

# add_str函数是一个参数是class,返回值也是class 的函数
def add_str(cls):
    """这个decorator本质就是重载了一下这个class的__str__方法"""
    def __str__(self):  # 自定义了一个名为__str__的函数,当然这个函数也可以叫任意名,但是为了规范我们不要随意起名
        return str(self.__dict__)
    # 将接收的class对象的__str__方法给替代成了add_str自己定义的__str__函数
    cls.__str__ = __str__
    return cls

# 等价于:MyObject = add_str(MyObject)
@add_str
class MyObject:
    def __init__(self, a, b):
        self.a = a
        self.b = b

o = MyObject(1, 2)
print(o)  # {'a': 1, 'b': 2}
  • 所以,给类写装饰器也没有什么难的,就是输入一个类返回一个类,然后在这个装饰器里面对传入的类动动手脚,完成我们需要的事情。

有参 可以装饰类的装饰器

# add_str函数是一个参数为greet的函数,返回一个参数为cls的inner函数
def add_str(greet):
    def inner(cls):
        print(greet)

        def say(self):
            return str(self.__dict__)

        # 将inner接收的class对象的__str__方法给替代成了inner自己定义的say函数
        cls.__str__ = say
        return cls

    return inner


# 等价于:MyObject = add_str("hello")(MyObject)
# 也就是:
# inner = add_str("hello")
# MyObject = inner(MyObject)
@add_str("hello")
class MyObject:
    def __init__(self, a, b):
        self.a = a
        self.b = b

"""
什么都不写,也会打印出hello,因为inner(MyObject)的时候,会执行inner函数,打印出hello
"""
o = MyObject(1, 2)
print(o)  # {'a': 1, 'b': 2}

如何把装饰器放到类里面

# 简单的装饰器,作用就是在函数运行之前打印start,在函数运行结束打印end
def log_function(func):
    def wrapper(*args, **kwargs):
        print("function start!")
        print(f"args: {args}")
        ret = func(*args, **kwargs)
        print(f"function end!")
        return ret
    return wrapper

@log_function
def fib(n):
    """斐波那契函数"""
    if n <= 1:
        return 0
    return fib(n - 1) + fib(n - 2)

fib(3)
"""
function start!
args: (3,)
function start!
args: (2,)
function start!
args: (1,)
function end!
function start!
args: (0,)
function end!
function end!
function start!
args: (1,)
function end!
function end!
"""

可以看到装饰器虽然用处不大,但是确实是工作了的,勉强算是能帮助我们理解递归。

我们一般写装饰器的时候,通常是把装饰器的代码定义在py文件的最外层,然而并不是所有的函数都适合放到全局空间里的。

如果我们就是想把装饰器放到类里面,或者说我们有若干个功能相似的装饰器,就是希望将其归类到同一个类里面,下面就让我们介绍一下如何把装饰器放到类里。

# 这是我们最容易想到的一种实现
class Decorators:
    def log_function(self, func):
        def wrapper(*args, **kwargs):
            print("function start!")
            print(f"args: {args}")
            ret = func(*args, **kwargs)
            print(f"function end!")
            return ret
        return wrapper

d = Decorators()  # <__main__.Decorators object at 0x10dab7fd0>

@d.log_function
def fib(n):
    if n <= 1:
        return 0
    return fib(n - 1) + fib(n - 2)

fib(3)

本质上就是把一个函数变成类的方法,所以这里我们仅仅是把函数加上self argument然后扔到类里面。

当我们使用这种方式的时候,你一定要意识到:一个类的方法,必须由这个类的实例对象所调用。所以当我们想使用这个装饰器的时候,我们首先要建立一个Decorators这个class的object,也就是12行代码,创建实例对象。

后面我们就可以用@d.log_function来装饰这个斐波那契函数。这种方式得到的结果和之前是一样的。

但是这样用是有问题的:

  • 非常明显的,我们为了使用这个装饰器,迫不得已的要建立一个新的对象。
  • 其次,我们这个装饰器里面莫名其妙的出现了self argument,然后我们这个装饰器压根就没有用到它。

为了解决问题:

  • 我们可以把类里面定义的方法变成class method,也就是把这个方法从对象调用变成一个类就可以调用的方法,我们使用@classmethod装饰器,那么在12行的时候就不需要新建一个对象才能使用这个装饰器了。我们可以直接在14行@Decorators.log_function来调用这个装饰器。

    • 但是这个class method的第一个argument依然是cls,我们依旧没有用到这个变量。

    • 如果我们这个装饰器会根据这个class或者这个object的变化而变化,从而改变装饰器自己的功能,那么在这种情况下,我们当然是可以这样写的。

    • 但是,如果我们只是单纯的想把装饰器放到类里面做一个封装,我们就可以考虑把class method变成一个static method。

    • 所以当你仅仅需要把一个装饰器封装到一个类里面的时候,下面的代码就是一个非常不错的方式:

      # 仅仅需要把一个装饰器封装到一个类里面的时候,这里的代码就是一个非常不错的实现
      class Decorators:
          @staticmethod
          def log_function(func):
              def wrapper(*args, **kwargs):
                  print("function start!")
                  print(f"args: {args}")
                  ret = func(*args, **kwargs)
                  print(f"function end!")
                  return ret
              return wrapper
      
      @Decorators.log_function
      def fib(n):
          if n <= 1:
              return 0
          return fib(n - 1) + fib(n - 2)
      
      fib(3)
      
  • 然而。。。还没完。如果我想用我类里面定义的属于这个类的装饰器去装饰我这个类里面的其他方法时,我应该怎么做?如下所示,怎么做?

    # 把fib函数移到了Decorators类里
    class Decorators:
        @staticmethod
        def log_function(func):
            def wrapper(*args, **kwargs):
                print("function start!")
                print(f"args: {args}")
                ret = func(*args, **kwargs)
                print(f"function end!")
                return ret
            return wrapper
    
        @log_function
        def fib(self, n):  # 在Python3.9中会报错TypeError: 'staticmethod' object is not callable;而在Python3.10中不会
            if n <= 1:
                return 0
            return self.fib(n - 1) + self.fib(n - 2)
    
    d = Decorators()
    d.fib(3)
    
  • 让我说的再明确点:我有一个类,类里面有很多的方法都希望用装饰器给它们装饰一下,但是我这个装饰器只会在这个类里面用,我又想把这个装饰器给封装到这个类里来。

正确的用法是:直接把log_function这个函数原封不动的放到这个Decorators class里

# 正确的做法,把fib函数原封不动的移到Decorators类里
class Decorators:
    def log_function(func):
        def wrapper(*args, **kwargs):
            print("function start!")
            print(f"args: {args}")
            ret = func(*args, **kwargs)
            print(f"function end!")
            return ret
        return wrapper

    @log_function
    def fib(self, n):
        if n <= 1:
            return 0
        return self.fib(n - 1) + self.fib(n - 2)

d = Decorators()
d.fib(3)

相信你,已经懵了。。你:“我从来没在Python里见到过长成这样的类的方法”。但是当你运行的时候你会神奇的发现这是可以运行的哦~

原因很简单:“当我们在定义一个新的class的时候,本质上是把class里面的code block在另外一个命名空间里运行了一下”。只不过我们非常不习惯看到这种没有self的method,但实际上这个log_function也不是一个method,你可以认为它是在我们定义这个类的时候里面的一个辅助函数或者是一个辅助的decorator。

意思就是说当我们尝试在这个类的定义之外 使用@d.log_function的时候,会报错。因为Python会尝试把这个log_funciton当成一个正常的对象方法去解读。

# 在类的定义之外使用d.log_function时报错
class Decorators:
    def log_function(func):
        def wrapper(*args, **kwargs):
            print("function start!")
            print(f"args: {args}")
            ret = func(*args, **kwargs)
            print(f"function end!")
            return ret
        return wrapper

    @log_function
    def fib(self, n):
        if n <= 1:
            return 0
        return self.fib(n - 1) + self.fib(n - 2)

d = Decorators()
@d.log_function
def f():  # TypeError: Decorators.log_function() takes 1 positional argument but 2 were given
    pass
f()

当然,我们我们直接使用@Decorators.log_function,则是可以工作的,因为@Decorators.log_function就是一个正常的函数。

# 直接使用 @Decorators.log_function 则是可以正常运行
class Decorators:
    def log_function(func):
        def wrapper(*args, **kwargs):
            print("function start!")
            print(f"args: {args}")
            ret = func(*args, **kwargs)
            print(f"function end!")
            return ret
        return wrapper

    @log_function
    def fib(self, n):
        if n <= 1:
            return 0
        return self.fib(n - 1) + self.fib(n - 2)

@Decorators.log_function
def f():
    pass
f()

最后,到底有没有一种方式,可以把我这个装饰器封装到类里,在这个类里可以正常使用;而在这个类之外,无论是通过实例对象调用、还是通过类调用都可以正常使用呢?

答案是有的!!!你只需要在类定义的最后加上log_function = staticmethod(log_function)这行代码即可

# 最终、最正确的实现方式
class Decorators:
    def log_function(func):
        def wrapper(*args, **kwargs):
            print("function start!")
            print(f"args: {args}")
            ret = func(*args, **kwargs)
            print(f"function end!")
            return ret
        return wrapper

    @log_function
    def fib(self, n):
        if n <= 1:
            return 0
        return self.fib(n - 1) + self.fib(n - 2)

    # 加上下面的代码,这行代码的本质和在log_function的定义上加一个@staticmethod这个装饰器是一样的,这两种方式是等价的
    # 注意区别:下面的方式在Python3.9中就不会报TypeError的错误了!
    # 只不过用@staticmethod装饰器的语法只能在定义log_function的同时去装饰它,而当我们使用@staticmethod装饰器的等价语法的时候,我们可以先定义log_function成为一个正常的函数,然后在class的definition里面把log_function当成一个正常的装饰器去使用,最后再通过log_function=staticmethod(log_function)把这个log_function变成一个static method
    log_function = staticmethod(log_function)


@Decorators.log_function
def f1():
    pass
f1()

d = Decorators()
@d.log_function
def f2():
    pass
f2()

通过上面的代码,现在无论我们是使用Decorators.log_function还是使用它的对象d.log_function,都能正确的执行这个装饰器的功能了。

@warps详解

__name__用来显示函数的名称,__doc__用来显示文档字符串也就是(“”“文档字符串”“”)这里面的内容。

文档字符串用于解释文档程序,帮助程序文档更加简单易懂。

可以在函数体的第一行使用一对三个单引号 ‘’’ 或者一对三个双引号 “”" 来定义文档字符串。

使用__doc__(注意双下划线)调用函数中的文档字符串属性。

装饰器中@wraps(view_func)的作用

不改变使用装饰器原有函数的结构(如name, doc)。

装饰器(decorator)在实现的时候,被装饰后的函数其实已经是另外一个函数了(函数名等函数属性会发生改变)。

为了不影响,Python的functools包中提供了一个叫**@wraps**的装饰器来消除这样的副作用。

写一个装饰器的时候,最好在实现之前加上functools中的wraps,它能保留原有函数的名称和文档字符串(DocStrings)。

不加@wraps的例子

def my_decorator(func):
    def wrapper(*args, **kwargs):
        """decorator"""
        print("Decorated function...")
        return func(*args, **kwargs)

    return wrapper


@my_decorator
def t():
    """TestWord"""
    print("Test function")


t()
# 输出:
# Decorated function...
# Test function

print(t.__name__)  # wrapper
print(t.__doc__)  # decorator

执行的整个过程:在调用t()函数时,首先会调用装饰器(将t作为参数传入到装饰器中),执wrapper函数,再执行t函数。

但我们可以看到t函数的名字:__name__为wrapper,__doc__为decorator,已经不是原来的test函数了。

加@wraps后

from functools import wraps


def my_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        """decorator"""
        print("Decorated function...")
        return func(*args, **kwargs)

    return wrapper


@my_decorator
def t():
    """TestWord"""
    print("Test function")


t()
# 输出:
# Decorated function...
# Test function

print(t.__name__)  # t
print(t.__doc__)  # TestWord

会发现,test函数的__name____doc__还是原来的,即函数名称和属性没有变换。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值