欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Python3 装饰器的的深入理解及实例参考

程序员文章站 2022-06-01 20:20:10
...

最初学习装饰器,经常会出现以下报错:
NoneType’ object is not callable
一般去掉调用函数后面的括号就可以解决。

import time
def printTime(f):
    def wrapper(*args, **kwargs):
        print("Time: ", time.ctime())
        return f(*args, **kwargs)
    return wrapper()
# 上面定义了一个装饰器。调用需要使用@
# hello函数就进行了扩展
@printTime
def hello():
    print("Hello world!")
hello

@printTime
def hello1():
    print("Hello world!")
hello1

输出结果:

Time:  Wed Jan  9 14:55:41 2019
Hello world!
Time:  Wed Jan  9 14:55:41 2019
Hello world!

网上查询了相关资料,深入理解装饰器的原理:
**

1. 什么是装饰器?

**

顾名思义,装饰器就是在方法上方标一个带有@符号的方法名,以此来对被装饰的方法进行点缀改造。

当你明白什么是装饰器之后,自然会觉得这个名字取得恰如其分,但作为初学者来说多少还是会有些迷茫。下面用代码来说明怎么理解装饰器。

#脚本1
def target():
print(‘this is target’)

def decorator(func):
func()
print(‘this is decorator’)

decorator(target)

运行结果为:

this is target
this is decorator

Python允许将方法当作参数传递,因此以上脚本就是将target方法作为参数传入decorator方法中,这其实也是装饰器的工作原理,以上代码等同于:

#脚本2
def decorator(func):
func()
print(‘this is decorator’)

@decorator
def target():
print(‘this is target’)

target

运行结果:

this is target
this is decorator

因此可以看出,所谓的装饰器就是利用了Python的方法可作参数传递的特性,将方法target作为参数传递到方法decorator中。

@decorator
def target():

这种在一个方法的上方加一个@符号的写法,就是表示位于下方的方法将被作为参数传递到位于@后面的decorator方法中。使用@符号只是让脚本1中的代码换了一个写法,更加好看,当然也会更加灵活与实用,后面会讲到这点。 但它们的本质其实是一样的,这也就是装饰器的工作原理。

**

2. 装饰器的原理

**

如果你仔细看的话,会在脚本2中发现一个问题,那就是脚本2中最后一行的target只是一个方法名字,它不是正确的方法调用,正确写法应该加上左右括号的target(),如下:

#脚本3

def decorator(func):
func()
print(‘this is decorator’)

@decorator
def target():
print(‘this is target’)

target()

运行结果:

this is target
this is decorator
Traceback (most recent call last):
File “C:/Users/Me/Desktop/ff.py”, line 34, in
target()
TypeError: ‘NoneType’ object is not callable

正如你所看到的,如果按照正确的写法,运行结果你会看到应该出现的两行打印文字"this is target"和"this is decorator",还会出现错误提示,ff.py是我为写这篇心得临时编写的一个py脚本名字,提示说’NoneType’对象不可调用。这是怎么回事?好吧,我现在必须告诉你,其实脚本2和脚本3中并不是一个使用装饰器的正确写法,不是使用错误,而是作为装饰器的decorator方法写的并不友好,是的,我不认为它是错误的写法,只是不友好。但只要你明白其中的道理,使用恰当的手段也是可以运行正常的,这就是为什么脚本2看似写错了调用方法却得出了正确的结果。当然学习还是得规规矩矩,后面我会具体说正确的装饰器怎么书写,在这里我先解释了一下脚本2和脚本3的运行原理,了解它们的运行原理和错误原因,其实就是了解装饰器的原理。

脚本2和脚本3的区别在于target和target(),也就是说真正的差别在于()这个括号。当()被附加在方法或者类后面时,表示调用,或者称为运行及实例化,无论称呼怎样,本质意义没有不同,都是调用给出的对象,当对象不具备调用性的时候,就会报错:‘某个类型’ object is not callable。当一个方法被调用后,即target(),是否能被再次执行,取决于它是否会return一个对象,并且该对象可以被调用。也许你会有点迷糊,对比一下代码会比较容易理解我想表达的意思:

1 >>>def returnme():
2 >>> print(‘this is returnme’)
3
4 >>>def target():
5 >>> print(‘this is target’)
6
7 >>>target
8 <function target at 0x00000000030A40D0>
9
10 >>>target()
11 target
12 <function returnme at 0x00000000030A4268>
13
14 >>>target()()
15 target
16 returnme
17
18 >>>returnme()()
19 returnme
20 Traceback (most recent call last):
21 File “<pyshell#15>”, line 1, in
22 returnme()()
23 TypeError: ‘NoneType’ object is not callable

如上所示,当直接在脚本中输入target,它只是告诉编译器(我想是编译器吧,因为我也不是很懂所谓编译器的部分),总之就是告诉那个不知道在哪个角落控制着所有python代码运行的“大脑”,在

0x00000000030A40D0位置(这个位置应该是指内存位置)存有一个function(方法)叫target;在target后面加上(),表示调用该方法,即输入target(),“大脑”便按照target方法所写的代码逐条执行,于是打印出了target字符串,并且“大脑”明白在0x00000000030A4268位置有一个叫returnme的方法;因为target对象调用后是会返回一个returnme方法,并且方法是可以被调用的,因此你可以直接这样书写target()(),“大脑”会逐条执行target中的代码,然后return一个returnme,因为多加了一个(),表示要对返回的returnme进行调用,于是再次逐条执行returnme中的代码,最后便能看到15、16的打印结果;而returnme方法是没有返回任何可调用的对象,因此当输入returnme()()时,“大脑”会报错。

下面我们可以来解释一下脚本2和脚本3的运行详情,之前说过,装饰器的工作原理就是脚本1代码所演示的那样。

@decorator
def target():

等同于

def decorator(target)():

注:python语法中以上写法是非法的,以上只是为了便于理解。

当你调用被装饰方法target时,其实首先被执行的是作为装饰器的decorator函数,然后“大脑”会把target方法作为参数传进去,于是:

#脚本2
def decorator(func):
func()
print(‘this is decorator’)

@decorator
def target():
print(‘this is target’)

target

实际运行情况:
首先调用decorator方法:decorator()
因为decorator方法含1个参数,因此将target传入:decorator(target)
运行代码“func()”,根据传入的参数,实际执行target(),结果打印出:this is target
运行代码"print(‘this is decorator’)",结果打印出:this is decorator

对比脚本3的运行情况:

#脚本3
def decorator(func):
func()
print(‘this is decorator’)

@decorator
def target():
print(‘this is target’)

target()

实际运行情况:
首先调用decorator方法:decorator()
因为decorator方法含1个参数,因此将target传入:decorator(target)
运行代码“func()”,根据传入的参数,实际执行target(),结果打印出:this is target
运行代码"print(‘this is decorator’)",结果打印出:this is decorator

以上与脚本2中运行情况完全相同,接下来便是执行脚本2中target没有的(),也就是执行调用命令。
由于decorator(target)没有返回一个可以被调用的对象,因此“大脑”提示错误:‘NoneType’ object is not callable

如果你还不是很清楚,请看下面的等价关系:

@decorator
def target():

等同于
def decorator(target)():

因此:
target == decorator(target)
target() == decorator(target)()

所以:
假设有一个变量var=target,在将target赋值给var时,其实是将decorator(target)的调用结果赋值给var,因为var不具备调用性(not callable),因此执行var()时,编译器会报错它是个NoneType对象,不能调用。

综上所述,你大概已经能够明白所谓的装饰器是怎么一回事,它是怎么工作的。但脚本2和脚本3中的写法会带来一些困惑,这个困惑就是通过我们编写的decorator装饰器对target进行装饰后,将target变成了一个永远不能被调用的方法,或者说变成了一个调用就报错的方法。这跟我们的使用习惯以及对方法的认识是很不协调的,毕竟我们还是习惯一个方法天生注定可以被调用这种看法。所以为了满足我们对方法的定义,我们最好将作为装饰器的方法写成一个可以返回具有被调用能力的对象的方法。

#脚本4
def whatever():
print(‘this is whatever’)

def decorator(func):
func()
print(‘this is decorator’)
return whatever  #1

@decorator
def target():
print(‘this is target’)


输入:target
结果:
this is target
this is decorator

输入:target()
结果:
this is target
this is decorator
this is whatever

在#1的位置,你可以return任何可以被调用的方法或类,甚至你可以直接写成:

def whatever():
print(‘this is whatever’)

def decorator(func):
return whatever

@decorator
def target():
print(‘this is target’)


输入:target
结果:告诉编译器在内存某个位置有一个叫whatever的方法

输入:target()
结果:this is whatever

以上装饰器的作用就是将target方法,完完全全变成了whatever方法。但这只能完美解释装饰器的功效,在实际当中则毫无意义,为什么要辛辛苦苦写了一大堆代码之后,最后的结果却是完完整整地去调用另一个早已存在的方法?如果要调用whatever方法,我为什么不在我的代码中直接写whatever呢?

装饰器,顾名思义,它就是要对被装饰的对象进行一些修改,通过再包装来达到不一样的效果,尽管它可以将被装饰对象变得面目全非或者直接变成另一个对象,但这不是它被发明出来的主要意义,相反它被发明出来是帮助我们更高效更简洁的编写代码,对其他方法或类进行装饰,而非摧毁它们。例如对接收数据的检校,我们只要写好一个校验方法,便可以在其他许多方法前作为装饰器使用。

**

3. 常规的装饰器

**

从广义上来说,装饰器就是脚本1中,利用python可以将方法传参的特性,用一个方法去改变另一个方法,只要改变成功,都可以认为是合格的装饰器。但这是理论上的合格,毕竟装饰器是一种语法糖,应该是为我们带来便利而不是无用功,所以:

  1. 将方法装饰成不能被调用,不好:
    def decorator(func):
    func()
    print(‘this is decorator’)

  2. 将原方法彻底消灭,直接变成另一个方法,不好:
    def decorator(func):
    return whatever

  3. 保留原方法的同时再加上别的功能,不好:
    def decorator(func):
    func()
    print(‘this is decorator’)
    return whatever

在以上3种写法中,前两种明显不好,简直就是将装饰器变成了恶魔。而第3种写法,虽然看起来重新包装了被修饰方法,但却在方法调用前擅自执行了一些命令,即当你输入target,而非target()时:

#脚本4
def whatever():
print(‘this is whatever’)

def decorator(func):
func()
print(‘this is decorator’)
return whatever  #1

@decorator
def target():
print(‘this is target’)


输入:target
结果:
this is target
this is decorator

你尚未执行target(),编译器却已经打印了两行字符串。这并不是我们想要的,当我们在代码中写下target时,我们是不希望编译器立即执行什么命令,我们是希望编译器在碰到target()时才执行运算。而且如果我们并不希望返回whatever,我们只想要通过装饰器,使得target方法除了打印自己的"this is target",再多打印一行"this is decorator”,所有代码只含有target和decorator两个方法,无其他方法介入,应该怎么办?

当你这样问的时候,其实就是已经开始了解装饰器存在的意义了。以下便是为解决这些问题的装饰器的常规写法:

#脚本5

def decorator(func):
def restructure():
func()
print(‘this is decorator’)
return restructure

@decorator
def target():
print(‘this is target’)

是的,从最外层讲,以上代码其实只有两个方法,decorator和target——装饰和被装饰方法。但在decorator内部内嵌了一个方法restructure,这个内嵌的方法才是真正改造target的东西,而decorator其实只是负责将target传入。这里的restructure,相当于在脚本4中,被写在decorator外面的whatever角色,用装饰器的常规写法也可以写出脚本4的效果,如下:

def decorator(func):
func()
print(‘this is decorator’)

def whatever():
    print('this is whatever')     
return restructure

@decorator
def target():
print(‘this is target’)

对比以上的写法你会发现,python的方法传参和类的继承性质很相似,在decorator之内,whatever之外,你可以写入任何代码,当执行target时,就开始初始化decorator,也就是执行decorator内部可以执行的代码;当执行target()时,会对初始化之后的decorator开始调用,因此这就要求decorator完成初始化之后必须返回一个具备调用性的对象。所以,除非你想要target方法初始化之前(实际上是对decorator进行初始化)就执行一些代码,否则不要在decorator和whatever中间插入代码。

正常情况下,当decorator完成初始化,应该return一个可调用对象,也就是脚本5中的restructure方法,这个方法就是替代target的克隆人,在restructure中你可以对target进行重写,或其他代码来包装target。因此你只是想初始化target的话(实际就是对restructure初始化),就应将你要初始化的代码写入到restructure内部去。另外你也可以在decorator中内嵌多个方法,或多层方法,例如:

#脚本6

def decorator(func):
def restructure():
func()
print(‘this is decorator’)

def whatever():
    func()
    print('this is whatever')

return restructure

@decorator
def target():
print(‘this is target’)

被decorator装饰的target最后会多打印一行’this is decorator’还是’this is whatever’,取决于decorator方法return的是它内部的哪一个方法(restructure或whatever),因此以上代码等价于以下写法:

执行target()

等同于

首先初始化decorator(target),结果返回一个restructure,即:target == decorator(target) == restructure。

然后调用,即:target() == decorator(target)() == restructure()

与类一样,当target被传入decorator之后,作为decorator内嵌方法是可以调用(继承)target方法的,这就是为什么restructure不用接受传参就可以改造target的原因。

注:在python中,以上多个()()的写法是非法的,这样写只是为了便于理解。

装饰器这个概念本来也可以设计成多个()这样的形式,但这样就破坏了python的基本写法,而且不好看,尤其当有多重装饰的时候,因此使用@置于方法前的设计是更加优雅和清晰的。

我之所以使用多个()这样并不被python支持的写法来阐述我对python的装饰器的理解,是因为我相信通过这样被python放弃的语法也能更好地帮助你理解python的继承、传参以及装饰器,尤其是带有参数的装饰器。

**

4. 装饰带有参数的方法

**

首先请看以下代码:

def target(x):
print(‘this is target %s’%x)

def decorator(func,x):
func(x)
print(‘this is decorator %s’%x)

decorator(target,’!’)

等同于:

def decorator(func):
def restructure(x):
func(x)
print(‘this is decorator %s’%x)
return restructure

@decorator
def target(x):
print(‘this is target %s’%x)

target(’!’)

target(x)中的参数x是如何传入,什么时候传入装饰器的?首先尝试以下代码:

def decorator(func):
print(x) #增加一行代码
def restructure(x):
func(x)
print(‘this is decorator %s’%x)
return restructure

@decorator
def target(x):
print(‘this is target %s’%x)

target(’!’)

此时编译器会报错参数x没有被定义,也就是说在初始化decorator的时候,只有target方法被传入,其参数x=’!'并没有传入。

现在让我们回顾一下之前说的装饰器工作原理,如下:

target == decorator(target) == restructure。

target() == decorator(target)() == restructure()

同理:

target(x) == decorator(target)(x) == restructure(x)

所以,你可以很清楚地了解到,作为target的参数,其实不是传给decorator,而是传给初始化完decorator之后return的restructure。因此,如果装饰器写成如下代码:

def decorator(func):
def restructure(): #不带参数的方法
func(x)
print(‘this is decorator %s’%x)
return restructure
此时你输入target(’!’),编译器会告诉你restructure没有参数,但被传入了1个参数,这个被传入的参数就是x=’!’。

所以你现在明白了被装饰的方法target(x),方法target和参数x是如何被传入的,所以你必须保证初始化decorator之后返回的对象restructure方法形参与被装饰的target方法形参相匹配,即:

如果定义为:def target(x)
则装饰器中:def restructure(x)

如果定义为:def target(x,y)
则装饰器中:def restructure(x,y)
你也许会发现如果想装饰器同时装饰target(x)和newtarget(x,y),以上写法就无法满足要求了。因此为了让装饰器可以适用于更多的对象,我们最好让装饰器写成如下形式:

def decorator(func):
def restructure(*x):
func(*x)
print(‘this is decorator’)
return restructure

@decorator
def target(x):
print(‘this is target %s’%x)

@decorator
def newtarget(x,y):
print(‘this is target %s%s’%(x,y))

target(’!’)
newtarget(’!’,’?’)

利用python的带星号参数语法(*arg),你便可以传入任意数量的参数,你也可以设置带双星号的形参(**arg),便可以传入字典形式的参数,单星形参和双星形参可以同时使用,如:def restructure(*arg, **arg)。

**

5. 带有参数的装饰器

**

只要记住以上的装饰器工作原理,你便可以知道如何写出带有参数的装饰器,如:

def newdecorator(i):
def decorator(func):
def restructure(x):
func(x)
print(‘this is decorator %s%s’%(i,x))
return restructure
return decorator

@newdecorator(’?’)
def target(x):
print(‘this is target %s’%x)

target(’!’)

结果:
this is target !
this is decorator ?!

以上代码实际上是:

target(x) == newdecorator(i)(target)(x) == decorator(target)(x) == reconstructure(x)
同理,为了满足不同数量的参数传入,你也可以将newdecorator(i)写成newdecorator(*i, **ii)。

**

6. 结束语

**

只要明白装饰器的设计原理,便可以自如地写出想要的装饰器,哪怕是多重、多参数的装饰器。

相关标签: 装饰器