社区所有版块导航
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

面试攻略 | 5分钟全面掌握 Python 装饰器

Python中文社区 • 5 年前 • 514 次点击  

投稿自 网易游戏运维平台 公众号:neteasegameops 


作者:吉星高照,网易游戏资深开发工程师,主要工作方向为网易游戏 CDN 自动化平台的设计和开发,脑洞比较奇特,喜欢在各种非主流的领域研究制作各种不走寻常路的东西。

Python的装饰器是面试的常客,因为其写法复杂多变,经常忘记什么地方应该写哪种参数,新手学习起来也经常一头雾水,不怕不怕,看了这一篇你对装饰器的各种用法就全明白了。废话不多说,直接进入主题!

不带参数的函数,不带参数的装饰器

我们先来写一个简单的装饰器,实现将函数运行前后的情况记录下来。

def dec1(func):
    print(func)
    def _wrap():
        print('before run')
        r = func()
        print('after run')
        return r
    return _wrap

@dec1
def f1():
    print('call f1')

上面只是定义了两个函数,运行后发现竟然有输出:

0x7fa1585f8488>

仔细看看,原来是第一个print语句的输出。这说明装饰的函数还没有实际运行的时候,装饰器就运行过了,因为@dec1相当于单独的一个语句:




    
dec1(f1)

那我们来正式运行一下f1吧:

f1(1)

输出如下确实达到了预期的效果:

before run
call f1
after run

不带参数的函数,带空参数的装饰器

后面我们还想要给装饰器加上参数呢,先试试用这个方式调用会发生什么情况:

@dec1()

输出了错误:

Traceback (most recent call last)
-268-01cf93cf6907> in 
      8     return _wrap
      9 
---> 10 @dec1()
     11 def f1():
     12     print('call f1')

TypeError: dec1() missing 1 required positional argument: 'func'

它说dec1需要接受func这个参数才行,那我们改改,作为f2函数吧:

def dec2():
    def _wrap(func):
        print(func)
        print('before run')
        return func
    return _wrap

@dec2()
def  f2():
    print('call f2')

f2()

这下可以了:

0x7fa1585af2f0>
before run
call f2

可是这个结构和原来有点不同了,而且,after run要写在哪里呢?很愁人地又改了一版,把它叫做f2x吧,对比dec1,又多了一层函数dec2x_w,开始有点晕:

def dec2x_w():
    def dec2x(func):
        print(func)
        def _wrap():
            print('before run')
            r = func()
            print('after run')
            return r
        return _wrap
    return dec2x

@dec2x_w()
def f2x():
    print('call f2x')

f2x()

运行一下看看,确实是想要的:

0x7fa1585af950>
before run
call f2x
after run

后面我们就不加before/after run了。

带参数的函数,不带参数的装饰器

函数f2x想要接受参数呢?我们把它叫做a吧,比较简单,不就是_wrap的参数吗,加上就是了,而且又回到了只有两层函数就可以实现了:

def dec3


    
(func):
    print(func)
    def _wrap(param):
        print(param)
        r = func(param)
        return r
    return _wrap

@dec3
def f3(a):
    print('call f3', a)

f3(1)

很争气地输出了结果:

0x7fa158719620>
1
call f3 1

带参数的函数,带参数的装饰器

下面我们实现一个装饰器,传入一个整数,和函数传入的参数做个加法:

def dec4_w(d_param):
    print(d_param)
    def dec4(func):
        print(func)
        def _wrap(param):
            print(param)
            r = func(param + d_param)
            return r
        return _wrap
    return dec4

@dec4_w(2)
def f4(a):
    print('call f4', a)

f4(1)

输出1+2=3

2
0x7fa1585af598>
1
call f4 3

从调用的装饰器往里看,注意这三层函数的形参,第一层是装饰器的参数,第二层是函数,第三层是函数的参数,很有规律的排列,先记一下这个规律(要考的)。带两个参数的也是一样的,接着写就可以了:




    
def dec5_w(d_param_1, d_param_2):
    print(d_param_1, d_param_2)
    def dec5(func):
        print(func)
        def _wrap(param):
            print(param)
            r = func(param + d_param_1 + d_param_2)
            return r
        return _wrap
    return dec5

@dec5_w(2, 3)
def f5(a):
    print('call f5', a)

f5(1)

输出1+2+3=6

2 3
0x7fa1586237b8>
1
call f5 6

如果用不定数量的位置参数,就用*args作为形参吧:

def dec6_w(*args):
    d_param_1, d_param_2, = args
    print(d_param_1, d_param_2)
    def dec6(func):
        print(func)
        def _wrap(*args):
            param = args[0]
            print(param)
            r = func(param + d_param_1 + d_param_2)
            return r
        return _wrap
    return dec6

@dec6_w(2, 3)
def f6(a):
    print('call f6', a)

f6(1)
print(f6.__name__)

顺便输出了一下f6的函数名:

2 3
0x7fa1586236a8>
1
call f6 6
_wrap

咦!怎么肥四!!!f6怎么是里面那个_wrap的名字呢?不怕不怕,functools提供了一个wraps装饰器专治各种不服(在装饰器里面放上另一个装饰器):

from functools import wraps

def dec7_w(*args):
    d_param_1, d_param_2, = args
    print(d_param_1, d_param_2)
    def dec7(func):
        print(func)
        @wraps(func)
        def _wrap(*args):
            param = args[0]
            print(param)
            r = func(param + d_param_1 + d_param_2)
            return r
        return _wrap
    return dec7

@dec7_w(2, 3)
def f7(a):
    print('call f7', a)

f7(1)
print(f7.__name__)

这下正常输出f7了:

2 3
0x7fa1585f8510>
1
call f7 6
f7

装饰器类(带参数的函数,带参数的装饰器)

用函数做装饰器局限性太多了,用相同的调用方法,把函数f7改成类怎么样?emmm…改造工程有点大,直接看看成品吧:

from functools import wraps

class dec8_c:
    def __init__(self, *args):
        self.d_param_1, self.d_param_2, = args
        print(self.d_param_1, self.d_param_2)
    def  __call__(self, func):
        print(func)
        @wraps(func)
        def _wrap(param):
            print(param)
            r = func(param + self.d_param_1 + self.d_param_2)
            return r
        return _wrap

@dec8_c(2, 3)
def f8(a):
    print('call f8', a)

f8(1)
print(f8.__name__)

看看是不是实现了一样的效果:

2 3
0x7fa1585f8048>
1
call f8 6
f8

虽然使用了__call__,但这里的__init__不能省略(因为它需要知道参数个数),否则会出现这个错误:

Traceback (most recent call last)
-276-1634a47057a2> in 
     14         return dec8
     15 
---> 16 @dec8_c(23)
     17 def f8(a):
     18      print('call f8', a)

TypeError: dec8_c() takes no arguments

同时还可以发现,__call__只需要两层函数了,去掉了第二层,直接把_wrap的函数体往上提了一层!

装饰器类(带参数的函数,不带参数的装饰器)

大概是吃饱了撑着,又想要实现一开始那个不带参数的装饰器了,那就继续敲敲打打一番看看:

class dec9_c:
    def  __init__(self, func):
        print(func)
        self.func = func
        self.__name__ = func.__name__
    def  __call__(self, param):
        print(param)
        func = self.func
        r = func(param)
        return r

@dec9_c
def f9(a):
    print('call f9', a)

f9(1)
print(f9.__name__)

赶快运行看看:

0x7fa1585f8730>
1
call f9 1
f9

咦,f9的函数名可以直接打印,这下都不用@wraps了呢!呃,再仔细看看,这写法好像有些不一样啊:

  • dec8_cinit带的是装饰器的参数,可是dec9_c带的是装饰器函数自己!

  • 所以实际调用的函数名也可以在init中传给它了哦!

  • 而且call函数也简洁了很多,看来有没有参数真的有很大区别呢!

这里先做个总结,装饰器使用函数名形式(不带括号)和使用函数调用形式(带括号和参数)在实现上是不同的,因为前者是函数本身,而后者是从装饰器函数中返回的函数。这也是f2相比f1缺少了记录after run的原因,因为dec1直接调用了f2,而dec2 先运行得到函数,再把函数返回去调用f2。用装饰器类就可以解决这个问题,因为它是对__call__的调用,只需要自己定义一下就可以了。

上面的f9要写两个函数,能不能写得和f1一样简洁?当然是可以的,使用__new__大法:

from functools import wraps

class dec9x_c:
    def  __new__(self, func):
        print(func)
        @wraps(func)
        def dec9x(param):
            print(param)
            r = func(param)
            return r
        return dec9x

@dec9x_c
def f9x(a):
    print('call f9x', a)

f9x(1)
print(f9x.__name__)

这样就避开了函数调用,不用打call了(定义__call__函数),快看它来了:

<function f9x at 0x7fa158623bf8>
1
call f9x 1
f9x

▼点击成为社区注册会员      喜欢文章,点个在看

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/52740
 
514 次点击