python装饰器进阶指南

2023-08-1808:22:23编程语言入门到精通Comments970 views字数 3739阅读模式

需求

我打算带着实际的需求来看待装饰器,这样也会更加容易理解。这道题目也是stackoverflow上的一个问题,我觉得很有意思,所以就拿来做例子。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/53933.html

@make_bold
@make_italic
def say():
   return "Hello"

运行say()将返回:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/53933.html

<b><i>Hello</i></b>

简单实现

# -*- coding: utf-8 -*-
import functools


def make_bold(fn):
    @functools.wraps(fn)
    def wrapper():
        return "<b>" + fn() + "</b>"

    return wrapper


def make_italic(fn):
    @functools.wraps(fn)
    def wrapper():
        return "<i>" + fn() + "</i>"

    return wrapper


@make_bold
@make_italic
def say():
    return "Hello"


if __name__ == '__main__':
    print(say())

看上去挺花哨,其实装饰器是一个python的语法糖,它的本质就是一个函数,它接受一个函数作为参数,并返回一个函数。我们可以把它理解为一个函数的包装器,它可以在不改变原函数的基础上,对函数进行增强。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/53933.html

我们尝试换种方式表达装饰器:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/53933.html

def say2():
    return "Hello"


if __name__ == '__main__':
    print(make_bold(make_italic(say2))())
    # <b><i>Hello</i></b>

可以发现,装饰器的作用就是把函数say2包装成了make_bold(make_italic(say2)),然后再调用它。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/53933.html

类方式装饰器

除了用函数的方式来实现装饰器,我们还可以用类的方式来实现装饰器。只需要实现__call__魔术方法即可。使得类的实例可以像函数一样被调用。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/53933.html


class MakeTag:
    def __init__(self, tag):
        self.tag = tag

    def __call__(self, fn):
        @functools.wraps(fn)
        def wrapper():
            return f"<{self.tag}>" + fn() + f"</{self.tag}>"

        return wrapper


make_bold = MakeTag('b')
make_italic = MakeTag('i')


@make_bold
@make_italic
def say3():
    return "Hello"

什么是functools.wraps

functools.wraps它也是一个装饰器,它能把原函数的一些属性复制到包装函数中,比如函数名、文档字符串、参数列表等。这样就不会出现一些奇怪的问题,比如我们在say函数上调用help(say),会发现它的文档字符串是wrapper函数的文档字符串,而不是say函数的文档字符串。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/53933.html

我们可以测试一下:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/53933.html

# 不带functools.wraps
def make_bold(fn):
    # @functools.wraps(fn)
    def wrapper():
        """wrapper help doc"""
        return "<b>" + fn() + "</b>"

    return wrapper


@make_bold
def say():
    """say something"""
    return "Hello"


print(say.__name__) # wrapper
print(say.__doc__)  # wrapper help doc

可以发现,在不被wraps装饰器装饰的情况下,say函数的__name____doc__属性都被改变了。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/53933.html

随后我们再测试一下带有functools.wraps的情况:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/53933.html

# 带functools.wraps
make_bold(fn):
    @functools.wraps(fn)
    def wrapper():
        """wrapper help doc"""
        return "<b>" + fn() + "</b>"

    return wrapper


@make_bold
def say():
    """say something"""
    return "Hello"


print(say.__name__) # say
print(say.__doc__)  # say something

因此,我们在编写装饰器的时候,最好都加上functools.wraps文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/53933.html

装饰器的参数

我将对上述的函数装饰器进行改造,使其可以接受参数。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/53933.html

也就是make_tag('b')将会生成make_bold()这样的形式。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/53933.html

def make_tag(tag):
    def decorator(fn):
        @functools.wraps(fn)
        def wrapper():
            return f"<{tag}>{fn()}</{tag}>"

        return wrapper

    return decorator


@make_tag('b')
@make_tag('i')
def say():
    return "Hello"

print(say())
# <b><i>Hello</i></b>

再有一种场景,现在我们say函数所返回的内容是固定的Hello,我们希望它可以接受参数,比如say('miclon'),这样就可以返回<b><i>miclon</i></b>文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/53933.html

@make_tag('b')
@make_tag('i')
def say(content):
    return content


print(say('miclon'))

如果我直接修改say函数,那么就会出现问题,因为say函数的参数列表已经发生了变化,而装饰器的参数列表压根没有参数列表,所以这样的修改是不行的。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/53933.html

TypeError: wrapper() takes 0 positional arguments but 1 was given文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/53933.html

为此我需要改进下装饰器的参数列表,使其可以接受参数。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/53933.html

def make_tag(tag):
    def decorator(fn):
        @functools.wraps(fn)
        def wrapper(content):
            return f"<{tag}>{fn(content)}</{tag}>"

        return wrapper

    return decorator

然而大部分情况下,我们不会这么"死板"地将装饰器的参数列表和被装饰函数的参数列表一一对应,这样不够灵活,也不便于代码维护。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/53933.html

因此,正确的做法是,我们将装饰器的参数列表设置为*args, **kwargs,这样就可以接受任意数量的参数了。换句话说,无论被装饰的函数有什么样的参数,我作为装饰器,被装饰函数的参数统统接受,并全部打回被装饰的函数。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/53933.html

def make_tag(tag):
    def decorator(fn):
        @functools.wraps(fn)
        def wrapper(*args, **kwargs):
            return f"<{tag}>{fn(*args, **kwargs)}</{tag}>"

        return wrapper

    return decorator

另类的装饰器

在众所周知的Django框架中,有一个这样的装饰器:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/53933.html

class classproperty:
    def __init__(self, method=None):
        self.fget = method

    def __get__(self, instance, cls=None):
        return self.fget(cls)

它的作用是,可以将一个类方法变成一个类属性,并且不再需要实例化对象后才可以调用。比如:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/53933.html

class Demo:

    @property
    def abc(self):
        return 123


print(Demo.abc)  # <property object at 0x1045708b0>
print(Demo().abc)  # 123

经过property的装饰,我们需要实例化对象后才可以调用abc属性。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/53933.html

那能不能不实例化也能调用呢?也就是换个思路,我得把abc变成一个"类属性",而不是实例属性。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/53933.html

我们回到上面classproperty装饰器上。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/53933.html

乍一看发现它并没有我上述所说的类装饰器的特性,它并没有实现__call__方法,那么它是如何实现装饰器的呢?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/53933.html

其实它这个classproperty类中的__get__方法是python中的描述符,用于代理另外一个类的属性,被装饰类方法首先会经过__init__,将类方法保存起来,然后再经过__get__,将类方法代理到类属性上。一旦属性被获取,就会触发__get__方法,通过self.fget(cls)调用类方法。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/53933.html

class classproperty:
    def __init__(self, method=None):
        self.fget = method

    def __get__(self, instance, cls=None):
        return self.fget(cls)


class Demo:

    @classproperty
    def abc(self):
        return 123


print(Demo.abc)  # 123

它将等同于:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/53933.html

class Demo:

  def _abc(self):
      return 123

  abc = classproperty(_abc)


print(Demo.abc)  # 123

 文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/53933.html

作者:it_miclon
来源:稀土掘金
文章源自菜鸟学院-https://www.cainiaoxueyuan.com/ymba/53933.html
  • 本站内容整理自互联网,仅提供信息存储空间服务,以方便学习之用。如对文章、图片、字体等版权有疑问,请在下方留言,管理员看到后,将第一时间进行处理。
  • 转载请务必保留本文链接:https://www.cainiaoxueyuan.com/ymba/53933.html

Comment

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定