1+1=10

扬长避短 vs 取长补短

Python装饰器小记

2001年,Python 2.2 引入了基于object的新式类,同时引入两个内置函数staticmethod和classmethod:

class C(object):
    def hello(arg1, arg2):
        ...
    hello = staticmethod(hello)

    def debao(cls, arg1, arg2):
        ...
    debao = classmethod(debao)

为了使写法更优雅,2004年发布的 Python 2.4 引入了装饰器(Decorator):

class C(object):
    @staticmethod
    def hello(arg1, arg2):
        ...
    @classmethod
    def debao(cls, arg1, arg2):
        ...

自此,装饰器在python中的使用越来越广

简单装饰器

如前所述,用于函数或方法的装饰器是 Python2.4 引入的,详见 PEP 318。后来 Python 3.0 时引入了用于类的装饰器,详见 PEP 3129。

简单来说,装饰器是一个函数:

  • 它接受一个函数作为输入并返回一个函数作为输出
  • 返回的函数可以与输入函数相同,也可以是不同的函数
  • 装饰器可以用于为输入函数添加附加功能而不修改输入函数

如果我们要给下面函数写一个装饰器:

def hello(name):
    print('hello', name)

由于函数在python中是一等公民,可以作为对象来传来传去,只需要这么写:

def decorator(func):
    def wrapper(*args, **kwargs):
        print('before')
        func(*args, **kwargs)
        print('after')
    return wrapper

而后就可以使用了:

@decorator
def hello(name):
    print('hello', name)

hello('debao')

结果:

before
hello debao
after

语法糖

首先装饰器是一个语法糖,如果不含糖,上面的写法就是:

hello = decorator(hello)
hello('debao')

或者:

decorator(hello)('debao')

内嵌函数、闭包(closure)

如果在一个内嵌函数中,对定义它的外部函数的作用域中的变量(甚至是外层之外,只要不是全局变量,也即内嵌函数中还可以嵌套定义内嵌函数)进行了引用,那么这个子函数就被认为是闭包。

定义装饰器时,首先定义了一个内嵌函数,而且该函数还访问了自身作用域外的变量 func。换句话说,它是一个闭包。

def decorator(func):
    def wrapper(*args, **kwargs):
        print('before')
        func(*args, **kwargs)
        print('after')
    return wrapper

注意,这个内嵌函数叫什么名字无所谓,只需要参数和待装饰函数的一致就好了。

装饰器类

除了可以用函数定义装饰器,还可以用类来定义装饰器:

class decorator_class:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print('before')
        self.func(*args, **kwargs)
        print('after')

@decorator_class
def hello(name):
    print('hello', name)

调用函数hello('debao')等价于:

decorator_class(hello)('debao')

挺简单直接的

装饰类方法

用法和装饰普通函数一样。但装饰类方法时,需要记得处理self参数。

class Hello:
    def __init__(self, name):
        self.name = name

    @decorator
    def say(self):
        print('hello', self.name)

前面我们参数类型用的 *args, **kwarg,直接隐藏了这个问题:

def decorator(func):
    def wrapper(*args, **kwargs):
        ...

不然就需要:

def decorator(func):
    def wrapper(self):
        print('before')
        func(self)
        print('after')
    return wrapper

带参数装饰器

要让装饰器带参数,只需要外边再包一层即可:

def count(c: int):
    def decorator(func):
        def wrapper(*args, **kwargs):
            print('before')
            for i in range(c):
                func(*args, **kwargs)
            print('after')
        return wrapper
    return decorator

@count(3)
def hello(name):
    print('hello', name)

hello('debao')

结果如下:

before
hello debao
hello debao
hello debao
after

也很直观,函数就是这么调用的:

count(3)(hello)('debao')

如果带参数的装饰器使用类定义,也是需要在外边套一层(效果是原来__init__的参数给了__call__,call里面使用闭包:

class Count:
    def __init__(self, c: int):
        self.c = c

    def __call__(self, func):
        def wrapper(*args, **kwargs):
            print('before')
            for i in range(self.c):
                func(*args, **kwargs)
            print('after')
        return wrapper

类装饰器

前面的装饰器,不管是用函数定义,还是用类定义,都是用于装饰函数的。PEP 3129 引入用于装饰类的装饰器。

类在Python中也是对象,可以写一个函数,输入是一个类,输出也是一个类:

def add_say_method(cls):
    def say(self):
        print('hello', self.name)
    cls.say = say
    return cls

@add_say_method
class Hello:
    def __init__(self, name):
        self.name = name

Hello('debao').say()

在这个例子中,我们给Hello类通过装饰器,添加了一个新的成员say。

如果去糖,则有:

add_say_method(Hello)('debao').say()

这儿,装饰器也可以用类来定义,也可以为装饰器添加参数。原理和用于函数的装饰器都是一样的。

装饰器嵌套

装饰器可以嵌套使用

@d1
@d2
def hello():
    pass

如果一个函数需要被多个装饰器装饰,执行顺序如何?:

def outer(c: int):
    print ('outer enter')
    def decorator(func):
        def wrapper(*args, **kwargs):
            print('outter before func')
            for i in range(c):
                func(*args, **kwargs)
            print('outter after func')
        return wrapper
    print ('outer leave')
    return decorator

def inner(func):
    print('inner enter')
    def wrapper(*args, **kwargs):
        print('inner before func')
        func(*args, **kwargs)
        print('inner after func')
    print('inner leave')
    return wrapper

@outer(3)
@inner
def hello(name):
    print('hello', name)

hello('debao')

结果:

outer enter
outer leave
inner enter
inner leave
outter before func
inner before func
hello debao
inner after func
inner before func
hello debao
inner after func
inner before func
hello debao
inner after func
outter after func

看结果稍微凌乱,但是执行顺序也简单:

outer(3)(inner(hello))('debao')

装饰器副作用

装饰器促进了代码复用,但是装饰后会丢失一些信息,比如函数的__name__等。

def decorator(func):
    def wrapper(*args, **kwargs):
        print('before')
        func(*args, **kwargs)
        print('after')
    return wrapper

@decorator
def hello(name):
    print('hello', name)

print (hello.__name__ )

结果并不是我们想要的:

wrapper

functools 模块的 wraps 用于缓解这种问题:

import functools
def decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print('before')
        func(*args, **kwargs)
        print('after')
    return wrapper

@decorator
def hello(name):
    print('hello', name)

print (hello.__name__ )

使用了wraps装饰器。

内置装饰器

具体清单见:

  • https://wiki.python.org/moin/Decorators

内置(builtin)装饰器:

  • staticmethod
  • classmethod
  • property

库中的装饰器:

  • functools.wraps
  • abc.abstractmethod: PEP 3119
  • dataclasses.dataclass:PEP 557
  • contextlib.contextmanager:PEP 343
  • typing.dataclass_transform:PEP 681
  • ...

  • contextlib.asynccontextmanager

  • ...

property

propery 用以将类中定义的对象方法变成属性,可以直接对属性赋值或读取:

class Student:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    @property
    def info(self):
        return self.name + ' ' + str(self.age)

    @info.setter
    def info(self, name_age):
        self.name, self.age = name_age.split(' ')

    @info.deleter
    def info(self):
        del self.name
        del self.age

    def __str__(self):
        return self.name + ' ' + str(self.age)

s = Student('1+1=10', 40)
print(s.info)

注意:property修饰的这几个方法的命名必须相同,getter对应的方法总是用 @property 修饰,其他两个为属性名加上 .setter.deleter,如果属性只读,不用定义 setter 方法。

functools.wraps

functools 中定义了好多装饰器。不过 wraps 是其中出镜频率最高的。

除此之外,还有:

  • functools.cache
  • functools.cached_property
  • functools.lru_cache
  • functools.total_ordering:类装饰符,被装饰的类必须实现__eq__以及一个不等操作。
  • functools.singledispatch
  • ....

可以看几个例子。

  1. 使用cache缓存程序的结果,比如:
import functools
@functools.cache
def factorial(n):
    return n * factorial(n-1) if n else 1

factorial(5)
print(factorial.cache_info())

结果

CacheInfo(hits=0, misses=6, maxsize=None, currsize=6)
  1. 使用singledispatch,感觉这个像C++中的模板或特化,比如:
import functools

@functools.singledispatch
def add(a, b):
    raise NotImplementedError('Unsupported type')

@add.register(int)
def _(a, b):
    print('First argument is of type int')
    print(a + b)

@add.register(str)
def _(a, b):
    print('First argument is of type str')
    print(a + b)

@add.register(list)
def _(a, b):
    print('First argument is of type list')
    print(a + b)

add(1, 2)             # OK
add('a', 'b')         # OK
add([1, 2], [3, 4])   # OK
add(1.0, 2.0)         # NotImplementedError

结果:

First argument is of type int
3
First argument is of type str
ab
First argument is of type list
[1, 2, 3, 4]
Traceback (most recent call last):
...
NotImplementedError: Unsupported type

abc.abstractmethod

Python3.0引入抽象基类abc(Abstract Base Classes),对应PEP 3119。同时引入了包括abstractmethod在内的多个装饰器:

from abc import ABCMeta, abstractmethod

class A(metaclass=ABCMeta):
    @abstractmethod
    def foo(self): pass

A()  # raises TypeError

class B(A):
    pass

B()  # raises TypeError

class C(A):
    def foo(self):
        print(42)

C()  # works

注意:装饰器abstractmethod,只能用在类内部,且该类的metaclass必须是ABCMeta或其派生类。

使用装饰器后,方法的__isabstractmethod__返回True:

from abc import ABCMeta, abstractmethod

class A(metaclass=ABCMeta):
    @abstractmethod
    def foo(self): pass

print (A.foo.__isabstractmethod__)

另外,abc中其他几个装饰器:

  • abstractproperty
  • abstractstaticmethod
  • abstractclassmethod

都已经废弃,不建议使用。

dataclass

例子:

from dataclasses import dataclass

@dataclass
class User:
    username: str
    email: str | None = None
    full_name: str | None = None
    disabled: bool | None = None

dataclass装饰器可以有参数:

@dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)
  • init:控制是否生成__init__方法;
  • repr:控制是否生成__repr__方法;
  • eq:控制是否生成__eq__方法,它用于判断实例是否相等;
  • order:控制是否创建四种大小关系方法:__lt____le____gt____ge__;order为True,则eq不能为False,也不能自定义order方法。
  • unsafe_hash:控制hash的生成方式。
  • frozen:控制是否冻结对field赋值。设置为True时,对象将是不可变的,__setattr____delattr__将会导致TypeError错误。

contextlib.contextmanager

with 语句相关

class C:
    def __init__(self):
        print('init')
    def __enter__(self):
        print('enter')
        return self
    def __exit__(self, exc_type, exc_val, exc_tb):
        print('exit')
        return True

with C() as a:
    print('hello')

使用装饰器:

@contextmanager
def A():
    print('enter')
    yield
    print('exit')

with A() as a:
    print('hello')

结果:

enter
hello
exit

注:与此类似,和 async with 配合的装饰器是 contextlib.asynccontextmanager

参考

Python

Comments