专栏首页疯狂软件李刚关于Python函数装饰器最简单的说明

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

导读

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

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

  • @staticmethod
  • @classmethod

比如如下代码:

class User:
    @classmethod
    def foo (cls):
        print("foo", cls)

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

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

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

一句话理解函数装饰器

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

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

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

注意

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

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

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

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

看下面代码:

# 带一个参数的函数,可作为装饰器函数
def foo (x):
    print('foo')

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

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

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

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

foo
------------
None

为什么会有这样的输出?

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

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

重点

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

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

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

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

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


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

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

装饰器函数为什么要参数

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

那么系列问题就来了:

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

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

# 带一个参数的函数,可作为装饰器函数
def foo (x):
    print('foo')
    print(x)

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

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

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

foo
<function bar at 0x0000000002911400>

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

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

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

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

实用的装饰器函数

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

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

# 带一个参数的函数,可作为装饰器函数
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表达式定义了一个参数。

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

foo
------------
<function foo.<locals>.<lambda> at 0x0000000002231510>
haha, 人生苦短,我用Python,学就要学疯狂Python讲义

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

函数装饰器与AOP

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

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

看下面程序:

# 带一个参数的函数,可作为装饰器函数
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吗?

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

本文结束

本文分享自微信公众号 - 疯狂软件李刚(fkbooks),作者:疯狂软件李刚

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-06-03

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 局部函数实现add(1)(2)(3)

    本文主要介绍如何通过局部函数(高阶函数)来实现函数curry,国内翻译为函数柯里化(这翻译太操蛋了)。这样可通过一个函数同时实现如下调用:

    疯狂软件李刚
  • 一小时掌握方法引用和构造器引用

    Java Lambda表达式是很常用的语法,Lambda表达式进一步简化就可写成方法引用和构造器引用。

    疯狂软件李刚
  • Python自动抢红包,超详细教程,再也不会错过微信红包了!

    提到抢红包,就不得不提Xposed框架,它简直是个抢红包的神器,但使用Xposed框架有一个前提条件:手机需要root,对于苹果手机的话就需要越狱了。现在的手机...

    疯狂软件李刚
  • 用函数式编程在 JS 中开发游戏

    一段时间以来,函数式编程范式比较火热,并且在互联网上有很多关于它的精彩书籍和文章,但是要找到相关程序的真实示例并不容易。因此,我决定尝试使用 Javascrip...

    疯狂的技术宅
  • swift之函数式编程

    最近初学swift,和OC比,发现语言更现代,也有了更多的特性。如何写好swift代码,也许,熟练使用新特性写出更优秀的代码,就是答案。今天先从大的方向谈谈sw...

    王大锤
  • 如何编写高质量的 JS 函数(4) --函数式编程[实战篇]

    本文会从如何用函数式编程思想编写高质量的函数、分析源码里面的技巧,以及实际工作中如何编写,来展示如何打通你的任督二脉。话不多说,下面就开始实战吧。

    2020labs小助手
  • 运用AOP思想更优雅地进行性能调优

    在软件测试中,如果想在一个耗时严重的操作中找出其耗时的瓶颈时,一般采用的方法是在每个被调用的函数中写进测试代码,在运行时打出日志。如果该操作涉及到的业务逻辑特别...

    腾讯移动品质中心TMQ
  • 初识函数式编程

    FinGet
  • 装饰器-初识

    函数调用的顺序 和其他语言类似。python函数在未经声明之前,不允许对其引用和调用。 ? 函数的功能与作用 需要添加一个打印日志功能。 在没学函数的时候,只能...

    企鹅号小编
  • [机器学习必知必会]机器学习是什么

    Tom Mitchell将机器学习任务定义为任务Task、训练过程Training Experience和模型性能Performance三个部分。 以分单引擎...

    TOMOCAT

扫码关注云+社区

领取腾讯云代金券