V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
KeepOnCode
V2EX  ›  推广

利用世界杯,读懂 Python 装饰器

  •  
  •   KeepOnCode · 2018-07-18 07:51:37 +08:00 · 5057 次点击
    这是一个创建于 2346 天前的主题,其中的信息可能已经有所发展或是发生改变。

    Python 装饰器是在面试过程高频被问到的问题,装饰器也是一个非常好用的特性, 熟练掌握装饰器会让你的编程思路更加宽广,程序也更加 pythonic。

    今天就结合最近的世界杯带大家理解下装饰器。

    德国战车

    6 月 17 日德国战墨西哥,小痴虽然是一个伪球迷,但每年的世界杯还是会了解下。而德国是上届的冠军,又是这届夺冠热门。德意志战车在 32 年间小组赛就没有输过!卧槽!虽然小痴很少赌球,但这次德国如此强大,肯定会赢吧。搏一搏单车变摩托!随后小痴买了德国队赢。心里想着这次肯定稳了!赢了会所嫩模!小痴连比赛都不看,美滋滋的敲着代码。

    然后比赛结果却是德国爆冷 0:1 输给墨西哥队,德国队输了比赛,小痴也下海干活。只是此时的天台有点挤,风还有大。

    小痴含泪的写下了下面的代码:

    def guess_win(func):
        def rooftop_status():
            result = func()
            print('天台已满,请排队!')
            return result
        return rooftop_status
    @guess_win
    def german_team():
        print('德国必胜!') 
    

    输出结果:

    德国必胜!
    天台已满,请排队! 
    

    装饰器是什么

    首先我们先来了解下什么是装饰器,严格来说,装饰器只是语法糖,装饰器是可调用的对象,可以像常规的可调用对象那样调用,特殊的地方是装饰器的参数是一个函数。

    装饰器的存在是为了适用两个场景,一个是增强被装饰函数的行为,另一个是代码重用。

    比如在上面的例子中我们在压德国队赢的时候,原本的 german_team() 函数只是输出德国必胜,但在使用装饰器( guess_win )后,它的功能多了一项:输出「天台已满,请排队!」。这就是一个简单的装饰器,实现了「增强被装饰函数的行为」。

    一个良好的装饰器必须要遵守两个原则:

    • 1 不能修改被装饰函数的代码

    • 2 不能修改被装饰函数的调用方式

    这里并不难以理解,在现在的生产环境中,很多代码是不能轻易的改写,因为这样有可能发送意想不到的影响。还有一点就是我们在看大神的代码,我们根本不懂如何改写。同时你也不能修改调用方式,因为你并不知道有在一个项目中,有多少处应用了此函数。

    装饰器理解基础

    如果你想要很好的理解装饰器,那下面的两个内容需要你先有所认知。

    • 1 函数名可以赋值给变量

    • 2 高阶函数

    1 函数名可以赋值给变量

    我们来看下这个例子:

    def func(name):
        print('我是{}!慌的一逼!'.format(name))
    func('梅西')
    y = func
    y('勒夫') 
    

    输出结果:

    我是梅西!慌的一逼!
    我是勒夫!慌的一逼!
    

    在代码中我们首先定义了函数 func,并调用了 func 函数,并且把 func 赋值给 y。y = func 表明了:函数名可以赋值给变量,并且不影响调用。

    这样讲,可能还有些人不太明白。我们在来对比下我们常用的操作。这其实和整数、数字是一样的,下面的代码你肯定熟悉:

    a = 1
    b = a
    print(a, b) 
    

    2 高阶函数

    高阶函数满足如下的两个条件中的任意一个:a.可以接收函数名作为实参; b.返回值中可以包含函数名。

    在 Python 标准库中的 map 和 filter 等函数就是高阶函数。

    l = [1, 2, 4]
    r = map(lambda x: x*3, l)
    for i in r:
        print('当前天台人数:', i) 
    

    输出结果:

    当前天台人数:3
    当前天台人数:6
    当前天台人数:12 
    

    自定义一个能返回函数的函数,也是高阶函数:

    def f(l):
        return map(lambda x: x *5, l)
    a = f(l)
    for i in a:
        print('当前天台人数:', i) 
    

    输出结果:

    当前天台人数:5
    当前天台人数:10
    当前天台人数:20 
    

    实现一个类似的装饰器

    现在你已经知道了「函数名赋值」和「高阶函数」,有了这两个基础,我们就可以尝试实现一个类似的装饰器。

    def status(func):
        print('慌的一逼!')
        return func
    def name():
        print('我是梅西!')
    temp = status(name)
    temp() 
    

    输出结果:

    慌的一逼!
    我是梅西! 
    

    在这个例子中我们定义了一个 status 函数,status 接收一个函数名然后直接返回该函数名。这样我们实现了不修改原函数 name,并且添加了一个新功能的需求。但是这里有个缺陷就是函数的调用方式改变了。即不是原本的 name,而是 temp。

    要解决这个问题很简单,相信 a = a*3 这样的表达式大家都见过,那么上述代码中的 temp = status(name) 同样可以修改为 name = status(name),这样我们就完美的解决了问题:既添加新功能又没有修改原函数和其调用方式。修改后的代码如下:

    def status(func):
        print('慌的一逼!')
        return func
    def name():
        print('我是梅西!')
    name = status(name)
    name() 
    

    但这样的代码却有个不便之处,即每次使用这样的装饰器,我们都要写类似 name = status(name) 的代码。程序员都是懒的,所以才有那么多高级的语法。在 python 中为了简化这种情况,提供了一个语法糖 @,在每个被装饰的函数上方使用这个语法糖就可以省掉这一句代码 name = status(name),最后的代码如下:

    def status(func):
        print('慌的一逼!')
        return func
    @status
    def name():
        print('我是梅西!')
    name() 
    

    这样我们就弄清楚了装饰器的工作原理:

    • 1 写一个高阶函数,即参数是函数,返回的也是函数。

    • 2 在利用语法糖 @,简化赋值操作。

    但是对比开头的例子,还是有些不一样。在开始的例子中,我们还实现了一个 rooftop_status 函数,来判断下当前的天台状是否人满。但是我们现在是直接返回了函数名,这样函数调用后我们就没办法做任何事情。梅西和德国慌了,我们也慌了,各个都要天台见,但在这之前我们也要考虑下天台的情况。

    为了能判断天台的情况,所以此时我们需要在嵌套一层函数,将实现额外功能的部分写在内层函数中,然后将这个内层函数返回即可。这也是为什么装饰器都是嵌套函数的原因。

    另外,开篇的例子并没有返回值,也没有参数,要对既有参数又有返回值的函数进行装饰的话,还需要进一步完善。 能够处理返回值的装饰器:

    def guess_win(func):
        def rooftop_status():
            result = func()
            print('天台已满,请排队!')
            return result
        return rooftop_status
    @guess_win
    def german_team():
        print('德国必胜!')
        return '赢了会所嫩模!输了下海干活!'
    x = german_team()
    print(x) 
    

    输出结果:

    德国必胜!
    天台已满,请排队!
    赢了会所嫩模!输了下海干活! 
    

    能够处理参数的装饰器:

    def guess_win(func):
        def rooftop_status(*args, **kwargs):
            result = func(*args, **kwargs)
            print('天台已满,请排队!')
            return result
        return rooftop_status
    @guess_win
    def german_team(arg):
        print('{}必胜!'.format(arg))
        return '赢了会所嫩模!输了下海干活!'
    x = german_team('德国')
    y = german_team('西班牙')
    print(x) 
    

    输出结果:

    德国必胜!
    天台已满,请排队!
    西班牙必胜!
    天台已满,请排队!
    赢了会所嫩模!输了下海干活! 
    

    总结

    装饰器的本质是函数,其参数是另一个函数(被装饰的函数)。装饰器通常会额外处理被装饰的函数,然后把它返回,或者将其替换成另一个函数或可调用对象。行为良好的装饰器可以重用,以减少代码量。

    对于这届的世界杯,我总结了下。

    本文首发与公众号「痴海」,后台回复「 1024 」,领取 2018 最新 python 教程。

    9 条回复    2018-07-21 19:13:08 +08:00
    ookkxw
        1
    ookkxw  
       2018-07-18 08:03:41 +08:00 via iPhone
    学习了,能大概看懂
    i730
        2
    i730  
       2018-07-18 08:10:00 +08:00
    写的很不错

    因为这样有可能发送意想不到的影响 : 发送 = 发生 吧?
    lhx2008
        3
    lhx2008  
       2018-07-18 08:22:08 +08:00 via Android
    JAVA 的 AOP 一把梭,doge
    qinrui
        4
    qinrui  
       2018-07-18 08:33:24 +08:00 via iPhone
    简单事情复杂化,无趣的蹭热点
    Hsinyao
        5
    Hsinyao  
       2018-07-18 08:42:07 +08:00 via iPhone
    感谢分享
    frozenthrone
        6
    frozenthrone  
       2018-07-18 08:54:11 +08:00 via iPhone
    v2 变简书了?
    ThirdFlame
        7
    ThirdFlame  
       2018-07-18 08:56:21 +08:00
    来学习习,还不太懂
    BingoXuan
        8
    BingoXuan  
       2018-07-18 09:17:35 +08:00
    本质其实就是闭包,通过装饰器将函数变成装饰器返回闭包函数调用,至于闭包过程做什么,自己随意定义。
    mingyun
        9
    mingyun  
       2018-07-21 19:13:08 +08:00
    感谢分享,终于明白了,有点像 PHP 的 trait
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1275 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 17:59 · PVG 01:59 · LAX 09:59 · JFK 12:59
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.