深入理解Python装饰器

时间:2023-02-24 16:05:36

By definition, a decorator is a function that takes another function and extends the behavior of the latter function without explicitly modifying it.

装饰器可以让我们在不修改一个函数的情况下,扩展函数的功能。让我们的代码更加优雅。

如何使用装饰器

我们的目标是,让函数每一次使用的时候都输出函数的使用时间。最简单的,我们可以这样:

import time

def print_time(func):
    print(time.strftime("Time: %H:%M:%S",time.gmtime()))
    func()

def foo():
    print("I'm foo")

print_time(foo)

但是这样,我们每次调用 foo 的时候都要调用 print_time(foo)。我们通过一个简单的装饰器来完成这个任务。我们用 wrapper 把函数包装了起来,并且这样还能很好的处理参数。

import time

def print_time(func):
    def wrapper(num):
        print(time.strftime("Time: %H:%M:%S",time.gmtime()))
        func(num)
    return wrapper

def foo(num):
    print("We are {} foo".format(num))

foo = print_time(foo)
foo(2)

Python 提供了一个语法糖 @,让我们用更简单的办法完成这件事情。@my_decoratorfunc = my_decorator(func) 的简单写法。

另外注意 wrapper(*args, **kwargs),这样就能处理任意多个参数的情况了。

import time

def print_time(func):
    #注意这个参数的写法,可以处理任意多的参数
    def wrapper(*args, **kwargs):
        print(time.strftime("Time: %H:%M:%S",time.gmtime()))
        #这样能处理有返回值的情况,虽然func没有
        return func(*args, **kwargs)
    return wrapper

#@语法糖
@print_time
def foo(num):
    print("We are {} foo".format(num))

foo(2)

还可以包装上额外的参数。@print_time 等价于 foo = print_time(foo),注意到函数 print_time 返回的是一个函数,就不难理解了。

另外用包装器封装会失去函数的 metadata(比如type hint),我们使用 @wraps(func) 来解决这个问题。通过这个装饰器把 func 的 metadata 复制到 decorator 里去。来看一个例子 @debug,提供调试信息的输出。注意 f-string 是 Python3.6 版本之后才有的新特性。

import functools

def debug(func):
    """Print the function signature and return value"""
    @functools.wraps(func)
    def wrapper_debug(*args, **kwargs):
        args_repr = [repr(a) for a in args]                      # 1
        kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]  # 2
        signature = ", ".join(args_repr + kwargs_repr)           # 3
        print(f"Calling {func.__name__}({signature})")
        value = func(*args, **kwargs)
        print(f"{func.__name__!r} returned {value!r}")           # 4
        return value
    return wrapper_debug

class A:
    @debug
    def func(self):
        pass

如果装饰器还需要参数,就这么写。上面的理解成 func = debug(func),下面的理解成 func = debug("Warning")(func)

def debug(text):
    def _debug(func):
        """Print the function signature and return value"""
        @functools.wraps(func)
        def wrapper_debug(*args, **kwargs):
            args_repr = [repr(a) for a in args]                      # 1
            kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]  # 2
            signature = ", ".join(args_repr + kwargs_repr)           # 3
            print(text, f"Calling {func.__name__}({signature})")
            value = func(*args, **kwargs)
            print(text, f"{func.__name__!r} returned {value!r}")           # 4
            return value
        return wrapper_debug
    return _debug


class A:
    @debug("Warning") 
    def func(self):
        pass

常用装饰器 :star:

内置装饰器:@staticmathod@classmethod@property。我们在介绍 python 的类的时候提到。

  • @property:把类内方法当成属性来使用,必须要有返回值,相当于 getter;
  • @staticmethod:静态方法,取消第一个参数 self。
  • @classmethod:类方法,第一个参数转变成表示自身类的 cls 参数。

在面向对象(OOP)的设计模式中,decorator 被称为装饰模式。和这个长得比较像的 Rust 的过程宏,他更加简单。

# @property的常用方法如下
class A:
    @property 
    def x(self):
        "I am the 'x' property." 
        return self._x
    
    @x.setter 
    def x(self, value):
        self._x = value

    @x.deleter 
    def x(self):
        del self._x
        
a = A()
a.x = 1     # setter
print(a.x)  # getter
del a.x     # deleter

深入理解装饰器

Python 什么时候运行装饰器

答案是,装饰器在函数定义之后立即运行,而不是在函数第一次执行时运行。其实原因也是比较显然的,你写 def 的时候产生一个实例嘛。

闭包

多数装饰器会修改被装饰的函数。通常会像上面的例子一样,定义一个内部函数,然后把它返回替换被装饰的函数。使用内部函数的代码几乎都要依靠闭包才能正常运作。

首先我们要了解 python 中变量的 作用域。变量有全局变量和局部变量。下面的例子中,a,c 就只有局部作用域,而 b 是全局变量。要在函数中引用全局变量,应该用 global 关键字声明。在 Python 中没有变量声明这个东西。对一个变量进行赋值的动作就是声明。所以在下面的例子中,如果你把 global b 删除,那么最后 print(b) 得到的将会是 1,函数中的 b 是局部变量,和全局变量 b 没有关系。

def f(a):
    global b
    c=1
    b=2
    print(a)
    print(b)
    print(c)

b=1
f(1)
print(b)

我们再来看这样一个求平均数的例子。每次调用 avg 都可以求之前所有数字的平均。可是,series 是 make_average 的局部变量,因为在那个函数中初始化了 series 变量。可是在我们调用 avg 函数的时候,make_average 早就已经返回了,它的本地作用域也一起不复返了。

对于 average 函数来说,series 是 *变量 (free variable),指未在本地作用域中绑定的变量。函数 average 的闭包包括了 series 的声明。不过我们之前也提到了,对一个变量的赋值在 python 中起到了声明的效果。所以和上面的 global 关键字类似,为了把变量声明为*变量而不是局部变量,我们使用 nonlocal 关键字。

def make_average():
    series = []
    def average(new):
        nonlocal series			#在这个函数中不是必要的,因为我们没有给series赋值
        series.append(new)
        total = sum(series)
        return total/len(series)
    return average

avg = make_average()

print(avg(1))
print(avg(3))

如果你想查看一下*变量,可以执行一下 print(avg.__code__.co_freevars)

函数就是对象

在 python 中函数就是一个对象,或者说,函数是一个 callable 的对象,对象如果 callable,那么它也能表现的像一个函数。怎么让一个函数 callable 呢?定义一个 __call__ 方法。

我们使用 dir 内置函数查看一下函数都有哪些属性,其中有一些属性是所有的对象共有的,有一些属性是所有的函数都有的:

>>> def a():
...     '''demo'''
...     return 1
...
>>> dir(a)
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
  • __name__:函数的名字,str
  • __globals__:函数所在模块中的全局变量,dict
  • __closure__:函数闭包,即*变量的绑定,tuple
  • __default__:形式参数的默认值,tuple
  • __annotations__:参数和返回值的注解,dict
  • __code__:编译成字节码的函数 metadata 和函数定义体