前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >关于Python函数装饰器最简单的说明

关于Python函数装饰器最简单的说明

作者头像
疯狂软件李刚
发布2020-06-24 17:02:32
7580
发布2020-06-24 17:02:32
举报
文章被收录于专栏:疯狂软件李刚疯狂软件李刚

导读

本文是关于Python函数装饰器最简单的介绍,没有废话,没有套路,赤裸裸的一句话就掌握Python函数装饰器。

学习Python的同学肯定见过下面两个东西:

  • @staticmethod
  • @classmethod

比如如下代码:

代码语言:javascript
复制
class User:
    @classmethod
    def foo (cls):
        print("foo", cls)

实际上,随着你用Python越来越多,你会发现这个东西在很多框架中都会出现。

对于Java转Python的同学来说,他们往往容易把这个东西当成注解——是这样理解吗?当然不是啦!Python就是Python,并不是Java,这个东西带@的东西在Python中叫函数装饰器。

正如前面提到的,随着你学Python越来越多,你会发现很多包或库的源代码大量使用了函数装饰器。由此可见,函数装饰器除了@classmethod、@staticmethod之外,普通开发者完全可以轻松自定义函数装饰器。

一句话理解函数装饰器

函数装饰器,不要被它的名字所迷惑了?你会想着,什么鬼函数装饰器,到底该怎么装饰啊?

实际上关于函数装饰器,只要一句话:函数装饰器就是函数替换的过程——被装饰的函数被替换成另一个东西。

那么问题来了,什么样的函数能作为装饰器函数呢?

注意

此处出现了两个概念,后文要仔细区分:

装饰器函数:用于修饰其他函数的东西。就是@符号后面的东西。

被装饰的函数,就是被@装饰器修饰的函数。

理论上来说,任意一个带一个形参的函数都可以作为装饰器函数。

看下面代码:

代码语言:javascript
复制
# 带一个参数的函数,可作为装饰器函数
def foo (x):
    print('foo')

@foo
#使用@foo装饰bar函数,bar函数是被装饰的函数
def bar ():
    print('bar')

print('------------')
print(bar)

在上面程序中,foo()函数带一个形参,因此它可作为装饰器函数。因此程序使用了@foo装饰了bar()函数。

执行上面程序,你会看到什么?它会产生如下输出:

代码语言:javascript
复制
foo
------------
None

为什么会有这样的输出?

上面1行输出表明foo()函数被调用了,而且在print('------------')之前输出。

程序3行输出显然对应于print(bar)的输出,这说明什么?这说明bar函数变成了None?这就是前面介绍的一句话:

重点

被装饰的函数被替换成另一个东西

被装饰的函数到底被替换成什么呢?答案是:被替换成装饰器函数的函数返回值!上面程序中foo()函数没有返回值——相当于返回值是None,因此被装饰的函数就被替换成了None,因此程序调用print(bar)就看到输出None了。

如果将程序改为如下形式:

代码语言:javascript
复制
# 带一个参数的函数,可作为装饰器函数
def foo (x):
    print('foo')
    return '疯狂Python讲义'

@foo
#使用@foo装饰bar函数,bar函数是被装饰的函数
def bar ():
    print('bar')


print('------------')
print(bar)

聪明的你,是否能猜出程序的运行结果?

装饰器函数为什么要参数

正如前面所说的:装饰器函数几乎没有要求,只要带一个形参!

那么系列问题就来了:

  • 装饰器函数的参数为什么是一个?不是两个?
  • 这个形参有什么用?
  • 装饰器函数什么时候调用?
  • 这个形参由谁来传入值?

为了解答这些问题,再看如下程序:

代码语言:javascript
复制
# 带一个参数的函数,可作为装饰器函数
def foo (x):
    print('foo')
    print(x)

@foo
#使用@foo装饰bar函数,bar函数是被装饰的函数
def bar ():
    print('bar')

在上面程序中同样定义了foo()函数作为装饰器函数,而bar()函数则被@foo装饰。乍一看,上面程序只是定义了两个函数,并没有调用语句,也没有任何输出。如果运行上面程序,会有输出吗?

你以为没有?运行上面程序,会看到如下输出:

代码语言:javascript
复制
foo
<function bar at 0x0000000002911400>

发现上面问题的答案了吗?

装饰器函数什么时候调用?每次你用”@装饰器函数“去装饰其他函数时,装饰器函数就会被调用。

这个形参由谁来传入值?Python会自动将被装饰的函数作为参数传入装饰器函数。因此上面程序中装饰器函数foo()中第二行输出x参数,输出的就是被装饰的bar()函数。

装饰器函数的参数为什么是一个?不是两个? 道理很简单,每次被装饰的函数只有一个,因此必须有一个、且只要一个参数来接收被装饰的函数。

实用的装饰器函数

前面的例子都很奇葩:装饰器函数直接把被装饰的函数替换成了None、或者str——这显然是太搞笑了,别人bar好歹是一个函数,但被你装饰之后,直接变成了None或者str,这也太不厚道了吧。

大部分时候,程序希望函数被装饰之后依然还是函数,那么该怎么办?很简单,只要让装饰器函数返回函数即可。例如如下代码。

代码语言:javascript
复制
# 带一个参数的函数,可作为装饰器函数
def foo (x):
    print('foo')
    return lambda y : print('haha,', y)

@foo
#使用@foo装饰bar函数,bar函数是被装饰的函数
def bar ():
    print('bar')

print('------------')
print(bar)
bar('人生苦短,我用Python,学就要学疯狂Python讲义')   #①

上面的装饰器函数foo()函数返回了一个lambda表达式——也就是相当于一个函数,这样被装饰的bar函数就会被替换成该lambda表达式,这样bar()函数就可以被调用了。

记住:bar()函数被彻底替换了!bar()函数被替换成了foo()函数返回的lambda表达式,因此上面程序①号代码表面上是调用bar函数,其实不是!其实是调用foo()函数所返回的lambda表达式!

因此你会看到,上面程序中定义bar()函数时并没有定义形参,但程序调用bar()函数(表面上是调用bar()函数,实际上是调用foo()函数返回的lambda表达式)却可以传入一个参数——这是因为foo()函数返回的lambda表达式定义了一个参数。

运行上面程序,可以看到如下输出。

代码语言:javascript
复制
foo
------------
<function foo.<locals>.<lambda> at 0x0000000002231510>
haha, 人生苦短,我用Python,学就要学疯狂Python讲义

从上面第3行输出可以看到,此时bar()函数已经替换成了lambda表达式,表面上是调用bar函数,实际上已经被替换成调用foo()函数的返回值。

函数装饰器与AOP

前面看到的例子依然有些奇葩:程序把被装饰的bar()函数完全替换掉了,bar()函数的执行逻辑完全被丢弃了,这怎么行呢?这对定义bar()函数的开发者的心灵伤害多大啊?别人辛辛苦苦定义了一个函数,但执行时完全一点用都不起?

更实用的情况是,函数装饰器会替换被装饰的函数,但它还会回调被装饰的函数的执行逻辑,只不过它会在被装饰的函数之前加入某种执行逻辑,也可在被装饰的函数之后加入某种执行逻辑——这不就是AOP的搞法吗?

看下面程序:

代码语言:javascript
复制
# 带一个参数的函数,可作为装饰器函数
def foo (x):
    # 定义一个函数,用于替换被装饰的函数
    # 由于程序无法确定被装饰的函数带几个形参,故此处使用了参数收集
    def __inner(*arg):
        print('==前置Advice==')
        # 回调被装饰的函数
        # 使用逆向参数收集
        x(*arg)  # ①
        print('==后置Advice==')
    # 返回inner函数
    return __inner

@foo
#使用@foo装饰bar函数,bar函数是被装饰的函数
def bar (name):
    print('bar', name)

print('------------')
bar('人生苦短,我用Python,学就要学疯狂Python讲义')

@foo
#使用@foo装饰moo函数,moo函数是被装饰的函数
def moo (a, b):
    print('moo', a, b)
moo(5, 20)

上面程序中foo()函数就是一个较为实用的函数装饰器了,该函数定义了一个__inner()嵌套函数,该函数用用于替换那些被装饰的函数——程序使用@foo装饰哪个函数,哪个函数就会被替换成__inner()函数。

由于程序无法确定@foo装饰的函数将会带几个形参——也就是无法确定调用__inner()函数时会传入几个参数,故此处使用了参数收集——类似于Java的个数可变的参数。

__inner()函数在①号代码处回调了被装饰的函数,这样就保留被装饰的函数的逻辑,而且可以在被装饰的函数执行之前织入Advice,也可以在被装饰的函数执行之后织入Advice。

对于装饰器函数来说,它既可在被装饰的函数之前织入Advice,也可在被装饰的函数执行之后织入Advice,它也可以访问或修改被装饰的函数的调用参数,也可访问被装饰的函数的返回值——这不就是典型的Around Advice吗?

可见,函数装饰器的功能非常具有想像力,它可以完成的功能将会非常强大。

本文结束

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-06-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 疯狂软件李刚 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档