Skip to content
0

Python装饰器

闭包

闭包是一种特殊的函数结构,它允许一个函数访问并操作其外部作用域中的变量。闭包的核心在于它能够“记住”并保留外部函数的局部变量,即使外部函数已经执行完毕,这些变量仍然可以被内部函数访问和使用。闭包通常由嵌套函数构成,即一个函数定义在另一个函数内部。

python
def line(a, b):
   def inner(x):
      return a*x+b
   return inner

# 相当于直线y=ax+b,设置a和b后,改变x即可求出y
l1 = line(2, 4)
print(l1(3))

需要注意的是:返回的函数并没有立刻执行,而是直到调用了才执行。

python
def build():
   fs = []
   for i in range(1, 4):
      def f():
         return i*i
      fs.append(f)
   return fs

f1, f2, f3 = build()
print(f1(), f2(), f3())  # 猜猜输出是什么?
查看答案

并不是"1, 4, 9",而是"9, 9, 9"。调用的时候i的值已经是3了。

常规装饰器

装饰器的作用是给函数添加额外的功能而无需改动原函数。

python
def decorator(func):
   print("此部分在装饰的时候就会运行")

   def inner(*args, **kwargs):
      print('---函数执行前---')
      res = func(*args, **kwargs)
      print("---函数执行后---")
      return res
   
   return inner

@decorator  # 装饰器装饰之后:func1 = decorator(func1)
def func1():
    return 'func1'

@decorator
def func2():
   return 'func2'

print(func1())
print(func2())

输出如下:

此部分在装饰的时候就会运行
此部分在装饰的时候就会运行
---函数执行前---
---函数执行后---
func1
---函数执行前---
---函数执行后---
func2

函数func1经过@decorator装饰之后就变成了inner函数,所以func1()实际执行的是inner(),原先的func1函数内容作为参数func传递给函数内部,根据python闭包的特性,此参数将会被保留下来,即使decorator函数运行完,此参数仍然可以被内部函数访问和使用。

带参装饰器

python
def wrap(param):
   def decorator(func):
      def inner(*args, **kwargs):
         print('inner: param=%s' % param)
         res = func(*args, **kwargs)
         return res
      return inner
   return decorator

# f1 = wrap('abc')(f1)
# 先运行wrap('abc')返回decorator函数,@decorator(f1)就相当于之前的常规装饰器了
@wrap('abc')
def f1():
    return 'f1'
python
from functools import partial

def decorator(func=None, *, name='aaa', age=18):
   if func is None:
      return partial(decorator, name=name, age=age)
   
   def inner(*args, **kwargs):
      print(f"inner: {name=}, {age=}")
      res = func(*args, **kwargs)
      return res
   return inner

# f1 = decorator(f1) 相当于常规装饰器
@decorator
def f1():
   print("f1")

# f2 = decorator(name='bbb', age=20)(f2)
# 先执行decorator(name='bbb', age=20),参数func为None,所以返回decorator函数
# 最后f2=decorator(f2) 相当于常规装饰器
@decorator(name='bbb', age=20)
def f2():
   print("f2")

装饰类的方法

装饰类的方法和装饰一般函数没有太大区别,唯一要注意装饰器和类方法装饰器的顺序不能弄混。

python
def stat_time(func):
   def inner(*args, **kwargs):
      ins = args[0]
      print(ins.name)
      start = time.time()
      res = func(*args, **kwargs)
      end = time.time()
      print("cost time: ", end - start)
      return res
   return inner

class Test:
   name = "xxx"

   @stat_time
   def instance_method(self, n):
      while n > 0:
         n -= 1
         time.sleep(0.1)

   @classmethod
   @stat_time
   def class_method(cls, n):
      while n > 0:
         n -= 1
         time.sleep(0.1)

   @staticmethod
   @stat_time
   def static_method(n):
      while n > 0:
         n -= 1
         time.sleep(0.1)

提示

第三方库wrapt在处理带参装饰器和装饰类的方法上更加简单,有兴趣可以阅读其文档了解。

关于函数签名问题

python
from functools import wraps

def decorator(func):
   @wraps(func)
   def inner(*args, **kwargs):
      res = func(*args, **kwargs)
      return res

   return inner

@decorator
def func1():
    """函数文档"""
    return 'func1'

# 不使用functools.wraps装饰时: inner None
#   使用functools.wraps装饰时:func1 函数文档
print(func1.__name__, func1.__doc__)

从上面的代码可以得出,装饰器会改变函数签名,需要使用functools.wraps装饰器来修正函数签名。事实上,functools.wraps也没完全修正函数签名,函数的参数签名仍然与原函数不一致,但对于大部分场景已经够用了。如果你想完全保持一致,可以使用第三方库👉decorator

python
from decorator import decorator
from inspect import getfullargspec
from functools import wraps

def use_wraps(func):
   @wraps(func)
   def inner(*args, **kwargs):
      print("calling %s with args %s, %s" % (func.__name__, args, kwargs))
      return func(*args, **kwargs)

   return inner

@decorator
def use_decorator(func, *args, **kwargs):
   print("calling %s with args %s, %s" % (func.__name__, args, kwargs))
   return func(*args, **kwargs)

def func1(a, *, b=None):
   pass

@use_wraps
def func2(a, *, b=None):
   pass

@use_decorator
def func3(a, *, b=None):
   pass

print(getfullargspec(func1))
print(getfullargspec(func2))
print(getfullargspec(func3))
func2()
# 输出结果如下:
# FullArgSpec(args=['a'], varargs=None, varkw=None, defaults=None, kwonlyargs=['b'], kwonlydefaults={'b': None}, annotations={})
# FullArgSpec(args=[], varargs='args', varkw='kwargs', defaults=None, kwonlyargs=[], kwonlydefaults=None, annotations={})
# FullArgSpec(args=['a'], varargs=None, varkw=None, defaults=None, kwonlyargs=['b'], kwonlydefaults={'b': None}, annotations={})
# calling func2 with args (1,), {}

从结果可以看出,decorator的参数签名与原函数一致。

装饰器结合函数签名的相关方法,可以验证函数的参数类型,给函数增加参数等,这里就不扩展了。一般开发搞不到这个层度。

类装饰器

python
class WrapClass:
   def __init__(self, class_name):
      self.cls = class_name

   def __call__(self, *args, **kwargs):
      print("你已经被装饰了")
      return self.cls(*args, **kwargs)

# Person = WrapClass(Person)  装饰之后Person变为WrapClass类的实例对象wrap_ins,
# 当其被实例化时,实际是wrap_ins(name, age),会调用__call__方法,返回是Person类的实例
@WrapClass
class Person:
   def __init__(self, name, age):
      self.name = name
      self.age = age

# 实例化的时候进行装饰
p1 = Person("wgj", 27)
python
class WrapClass:
   def __init__(self, level=1):
      self.level = level

   def __call__(self, cls):
      @wraps(cls)
      def wrap(*args, **kwargs):
         if self.level > 1:
            print(f"当前等级:{self.level}")
         return cls(*args, **kwargs)
      return wrap

# Person = WrapClass(2)(Person)  
# 本质和上面的无参形式没啥区别,只是实例对象有初始化参数。
@WrapClass(2)
class Person:
   def __init__(self, name, age):
      self.name = name
      self.age = age

__call__方法可以让类的实例像函数一样被调用。

下面看一个类装饰器实现的给函数扩充功能的例子:

python
import types
from functools import partial, update_wrapper
import time


class DelayFunc:
   def __init__(self, func, duration=1):
      self.duration = duration
      self.__func = func
      update_wrapper(self, func)   # == wraps(func)(self)

   def __call__(self, *args, **kwargs):
      print(f'Wait for {self.duration} seconds... {args}')
      time.sleep(self.duration)
      return self.__func(*args, **kwargs)

   # 让此装饰器可以装饰类的函数
   def __get__(self, instance, owner):
      if instance is None:
         return self
      # 返回一个函数,此函数的第一参数是实例instance(相当于实例方法)。
      # 由于self成为可调用对象是通过__call__实现的,
      # 所以当调用__call__时,其args的第一个参数就是这个实例instance
      method = types.MethodType(self, instance)
      return method

   def eager_call(self, *args, **kwargs):
      return self.__func(*args, **kwargs)

def delay(duration):
    """
    装饰器:推迟某个函数的执行。同时提供 .eager_call 方法立即执行。
    为了避免定义额外函数,直接使用 functools.partial 帮助构造 DelayFunc 实例
    :param duration:
    :return:
    """
    return partial(DelayFunc, duration=duration)

# delay(duration=2) -> @DelayFunc(duration=2) -> add = DelayFunc(add, duration=2)
@delay(2)
def add(a, b):
    print(a + b)

class Spam:
   # add = DelayFunc(add, duration=2) add是DelayFunc的实例对象df了
   # Spam的实例调用add方法时,由于add是非数据描述器DelayFunc的实例,
   # 根据描述器协议:描述器的实例被另一个类访问时,__get__会被调用。
   # df() 将调用__call__,其args=(self, a, b),就是下面的三个参数
   @delay(2)
   def add(self, a, b):
      print(a + b)


# add已经是DelayFunc的实例,但它也有函数的部分属性(归功于update_wrapper)
print(add, add.__name__)
add(1, 2)  # 这次调用将会延迟 2 秒
add.eager_call(1, 2) # 这次调用将会立即执行,用类装饰为函数添加了新的功能

s = Spam()
s.add(1, 2)