资讯专栏INFORMATION COLUMN

简单理解Python装饰器

Meils / 3080人阅读

摘要:下面我们一起抛去无关概念,简单地理解下的装饰器。用函数实现装饰器装饰器要求入参是函数对象,返回值是函数对象,嵌套函数完全能胜任。为了对调用方透明,装饰器返回的对象要伪装成被装饰的函数。

来源:http://www.lightxue.com/under...

       Python有大量强大又贴心的特性,如果要列个最受欢迎排行榜,那么装饰器绝对会在其中。
       刚接触装饰器,会觉得代码不多却难以理解。其实装饰器的语法本身挺简单的,复杂是因为同时混杂了其它的概念。下面我们一起抛去无关概念,简单地理解下Python的装饰器。

装饰器的原理

       在解释器下跑个装饰器的例子,直观地感受一下

# make_bold就是装饰器,实现方式这里略去

>>> @make_bold
... def get_content():
...     return "hello world"
...
>>> get_content()
"hello world"

       被make_bold装饰的get_content,调用后返回结果会自动被b标签包住。怎么做到的呢,简单4步就能明白了。

1. 函数是对象

       我们定义个get_content函数。这时get_content也是个对象,它能做所有对象的操作。

它有id,有type,有值。

>>> id(get_content)
140090200473112
>>> type(get_content)

>>> get_content

跟其他对象一样可以被赋值给其它变量。

>>> func_name = get_content
>>> func_name()
"hello world"

它可以当参数传递,也可以当返回值

>>> def foo(bar):
...     print(bar())
...     return bar
...
>>> func = foo(get_content)
hello world
>>> func()
"hello world"
2. 自定义函数对象

       我们可以用class来构造函数对象。有成员函数__call__的就是函数对象了,函数对象被调用时正是调用的__call__。

class FuncObj(object):
    def __init__(self, name):
        print("Initialize")
        self.name= name

    def __call__(self):
        print("Hi", self.name)

我们来调用看看。可以看到,函数对象的使用分两步:构造和调用(同学们注意了,这是考点)。

>>> fo = FuncObj("python")
    Initialize
>>> fo()
    "Hi python"
3. @是个语法糖

装饰器的@没有做什么特别的事,不用它也可以实现一样的功能,只不过需要更多的代码。

@make_bold
def get_content():
    return "hello world"

上面的代码等价于下面的

def get_content():
    return "hello world"
get_content = make_bold(get_content)

make_bold是个函数,要求入参是函数对象,返回值是函数对象。@的语法糖其实是省去了上面最后一行代码,使可读性更好。用了装饰器后,每次调用get_content,真正调用的是make_bold返回的函数对象。

4. 用类实现装饰器

入参是函数对象,返回是函数对象,如果第2步里的类的构造函数改成入参是个函数对象,不就正好符合要求吗?我们来试试实现make_bold。

class make_bold(object):
    def __init__(self, func):
        print("Initialize")
        self.func = func

    def __call__(self):
        print("Call")
        return "{}".format(self.func())

大功告成,看看能不能用。

>>> @make_bold
... def get_content():
...     return "hello world"
...
Initialize
>>> get_content()
Call
"hello world"

成功实现装饰器!是不是很简单?
这里分析一下之前强调的构造和调用两个过程。我们去掉@语法糖好理解一些。

# 构造,使用装饰器时构造函数对象,调用了__init__
>>> get_content = make_bold(get_content)
Initialize

# 调用,实际上直接调用的是make_bold构造出来的函数对象
>>> get_content()
Call
"hello world"

到这里就彻底清楚了,完结撒花,可以关掉网页了~~~(如果只是想知道装饰器原理的话)


函数版装饰器

阅读源码时,经常见到用嵌套函数实现的装饰器,怎么理解?同样仅需4步。

1. def的函数对象初始化

用class实现的函数对象很容易看到什么时候构造的,那def定义的函数对象什么时候构造的呢?

# 这里的全局变量删去了无关的内容
>>> globals()
{}
>>> def func():
...     pass
...
>>> globals()
{"func": }

不像一些编译型语言,程序在启动时函数已经构造那好了。上面的例子可以看到,执行到def会才构造出一个函数对象,并赋值给变量make_bold。

这段代码和下面的代码效果是很像的。

class NoName(object):
    def __call__(self):
        pass
func = NoName()
2. 嵌套函数

Python的函数可以嵌套定义。

def outer():
    print("Before def:", locals())
    def inner():
        pass
    print("After def:", locals())
    return inner
    
#inner是在outer内定义的,所以算outer的局部变量。
#执行到def inner时函数对象才创建,因此每次调用outer都会创建一个新的inner。
#下面可以看出,每次返回的inner是不同的。

>>> outer()
Before def: {}
After def: {"inner": .inner at 0x7f0b18fa0048>}
.inner at 0x7f0b18fa0048>
>>> outer()
Before def: {}
After def: {"inner": .inner at 0x7f0b18fa00d0>}
.inner at 0x7f0b18fa00d0>
3. 闭包

嵌套函数有什么特别之处?因为有闭包。

def outer():
    msg = "hello world"
    def inner():
        print(msg)
    return inner

下面的试验表明,inner可以访问到outer的局部变量msg。

>>> func = outer()
>>> func()
hello world

闭包有2个特点
inner能访问outer及其祖先函数的命名空间内的变量(局部变量,函数参数)。
调用outer已经返回了,但是它的命名空间被返回的inner对象引用,所以还不会被回收。
这部分想深入可以去了解Python的LEGB规则。

4. 用函数实现装饰器

装饰器要求入参是函数对象,返回值是函数对象,嵌套函数完全能胜任。

def make_bold(func):
    print("Initialize")
    def wrapper():
        print("Call")
        return "{}".format(func())
    return wrapper

用法跟类实现的装饰器一样。可以去掉@语法糖分析下构造和调用的时机。

>>> @make_bold
... def get_content():
...     return "hello world"
...
Initialize
>>> get_content()
Call
"hello world"

因为返回的wrapper还在引用着,所以存在于make_bold命名空间的func不会消失。make_bold可以装饰多个函数,wrapper不会调用混淆,因为每次调用make_bold,都会有创建新的命名空间和新的wrapper。
到此函数实现装饰器也理清楚了,完结撒花,可以关掉网页了~~~(后面是使用装饰的常见问题)

常见问题 1. 怎么实现带参数的装饰器?

带参数的装饰器,有时会异常的好用。我们看个例子。

    >>> @make_header(2)
    ... def get_content():
    ...     return "hello world"
    ...
    >>> get_content()
    "

hello world

" #怎么做到的呢?其实这跟装饰器语法没什么关系。去掉@语法糖会变得很容易理解。 @make_header(2) def get_content(): return "hello world" # 等价于 def get_content(): return "hello world" unnamed_decorator = make_header(2) get_content = unnamed_decorator(get_content)

上面代码中的unnamed_decorator才是真正的装饰器,make_header是个普通的函数,它的返回值是装饰器。

来看一下实现的代码。

def make_header(level):
    print("Create decorator")

    # 这部分跟通常的装饰器一样,只是wrapper通过闭包访问了变量level
    def decorator(func):
        print("Initialize")
        def wrapper():
            print("Call")
            return "{1}".format(level, func())
        return wrapper

    # make_header返回装饰器
    return decorator

看了实现代码,装饰器的构造和调用的时序已经很清楚了。

>>> @make_header(2)
... def get_content():
...     return "hello world"
...
Create decorator
Initialize
>>> get_content()
Call
"

hello world

"
2. 如何装饰有参数的函数?

为了有条理地理解装饰器,之前例子里的被装饰函数有意设计成无参的。我们来看个例子。

@make_bold
def get_login_tip(name):
    return "Welcome back, {}".format(name)

最直接的想法是把get_login_tip的参数透传下去。

class make_bold(object):
    def __init__(self, func):
        self.func = func

    def __call__(self, name):
        return "{}".format(self.func(name))

       如果被装饰的函数参数是明确固定的,这么写是没有问题的。但是make_bold明显不是这种场景。它既需要装饰没有参数的get_content,又需要装饰有参数的get_login_tip。这时候就需要可变参数了。

class make_bold(object):
    def __init__(self, func):
        self.func = func
    def __call__(self, *args, **kwargs):
        return "{}".format(self.func(*args, **kwargs))

       当装饰器不关心被装饰函数的参数,或是被装饰函数的参数多种多样的时候,可变参数非常合适。可变参数不属于装饰器的语法内容,这里就不深入探讨了。

3. 一个函数能否被多个装饰器装饰?

下面这么写合法吗?

@make_italic
@make_bold
def get_content():
    return "hello world"

合法。上面的的代码和下面等价,留意一下装饰的顺序。

def get_content():
    return "hello world"
get_content = make_bold(get_content) # 先装饰离函数定义近的
get_content = make_italic(get_content)
4. functools.wraps有什么用?

       Python的装饰器倍感贴心的地方是对调用方透明。调用方完全不知道也不需要知道调用的函数被装饰了。这样我们就能在调用方的代码完全不改动的前提下,给函数patch功能。
       为了对调用方透明,装饰器返回的对象要伪装成被装饰的函数。伪装得越像,对调用方来说差异越小。有时光伪装函数名和参数是不够的,因为Python的函数对象有一些元信息调用方可能读取了。为了连这些元信息也伪装上,functools.wraps出场了。它能用于把被调用函数的__module__,__name__,__qualname__,__doc__,__annotations__赋值给装饰器返回的函数对象。

import functools
def make_bold(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return "{}".format(func(*args, **kwargs))
    return wrapper

对比一下效果。

>>> @make_bold
... def get_content():
...     """Return page content"""
...     return "hello world"
>>>
# 不用functools.wraps的结果
>>> get_content.__name__
"wrapper"
>>> get_content.__doc__
>>>
# 用functools.wraps的结果
>>> get_content.__name__
"get_content"
>>> get_content.__doc__
"Return page content"

       实现装饰器时往往不知道调用方会怎么用,所以养成好习惯加上functools.wraps吧。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/40872.html

相关文章

  • python奇遇记:深入理解装饰

    摘要:可见装饰器改变了函数的功能。装饰器除了改变函数功能之外还有一个特性是,函数装饰器在导入模块时立即执行,而被装饰的函数只在明确调用时运行。 什么是装饰器 装饰器是什么,简单来说,装饰器可以改变一个函数的行为,比如原本有一个函数用来计算菲波那切数列,我们给这个函数加个计算执行时间的装饰器,这样原来的函数不仅能够计算菲波那切数列,而且还可以输出计算花费了多少时间。 在Python中,有几个很...

    lemon 评论0 收藏0
  • Python知识点:理解和使用装饰 @decorator

    摘要:使用类装饰器,优点是灵活性大,高内聚,封装性。不过不用担心,有,本身也是一个装饰器,它的作用就是把原函数的元信息拷贝到装饰器函数中,使得装饰器函数也有和原函数一样的元信息。 showImg(https://segmentfault.com/img/bVbrFWb?w=742&h=484);Python的装饰器(decorator)是一个很棒的机制,也是熟练运用Python的必杀技之一。...

    cyqian 评论0 收藏0
  • python装饰入门小结

    摘要:使用一年多了,一直知道有个装饰器,很好用,试图理解过,可能由于资料找的不好,自己的悟性太差,一直没有搞清楚,今天查了一些资料,算是理解了,现在简单记录下。 使用python一年多了,一直知道python有个装饰器,很好用,试图理解过,可能由于资料找的不好,自己的悟性太差,一直没有搞清楚,今天查了一些资料,算是理解了,现在简单记录下。python的装饰器本身的功能是在不改变已有函数本身的...

    SunZhaopeng 评论0 收藏0
  • Python】一文弄懂python装饰(附源码例子)

    摘要:装饰器的使用符合了面向对象编程的开放封闭原则。三简单的装饰器基于上面的函数执行时间的需求,我们就手写一个简单的装饰器进行实现。函数体就是要实现装饰器的内容。类装饰器的实现是调用了类里面的函数。类装饰器的写法比我们装饰器函数的写法更加简单。 目录 前言 一、什么是装饰器 二、为什么要用装饰器 ...

    liuchengxu 评论0 收藏0
  • Python: 会打扮的装饰

    摘要:一般情况下,我们使用装饰器提供的语法糖,来简化上面的写法像上面的情况,可以动态修改函数或类功能的函数就是装饰器。本文标题为会打扮的装饰器本文链接为参考资料修饰器的函数式编程中的装饰器介绍思诚之道装饰器入门与提高赖明星 装饰器 我们知道,在 Python 中,我们可以像使用变量一样使用函数: 函数可以被赋值给其他变量 函数可以被删除 可以在函数里面再定义函数 函数可以作为参数传递给另外...

    blastz 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<