社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

简单地理解 Python 的装饰器

Python开发者 • 7 年前 • 705 次点击  

(点击上方蓝字,快速关注我们)


来源:0xFEE1C001

www.lightxue.com/understand-python-decorator-the-easy-way

如有好文章投稿,请点击 → 这里了解详情


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也是个对象,它能做所有对象的操作。


def get_content():

    return 'hello world'


它有id,有type,有值。


>>> id(get_content)

140090200473112

>>> type(get_content)

<class 'function'>

>>> get_content

<function get_content at 0x7f694aa2be18>


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


>>> 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': <function func at 0x10f5baf28>}


不像一些编译型语言,程序在启动时函数已经构造那好了。上面的例子可以看到,执行到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': <function outer.< locals>.inner at 0x7f0b18fa0048>}

<function outer.<locals>.inner at 0x7f0b18fa0048>

>>> outer()

Before def: {}

After def: {'inner': <function outer.<locals>.inner at 0x7f0b18fa00d0>}

<function outer.<locals>.inner at 0x7f0b18fa00d0>


3. 闭包


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


def outer():

    msg = 'hello world'

    def inner ():

        print(msg)

    return inner


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


>>> func = outer()

>>> func()

hello world


闭包有2个特点


  1. inner能访问outer及其祖先函数的命名空间内的变量(局部变量,函数参数)。

  2. 调用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吧。


这次是真•完结了,有疑问请留言,撒花吧~~~


看完本文有收获?请转发分享给更多人

关注「Python开发者」,提升Python技能


今天看啥 - 高品质阅读平台
本文地址:http://www.jintiankansha.me/t/1utRJMHuOq
Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/3505
 
705 次点击