一起来理解Python中的装饰器的本质

函数是一个对象

Python中万物皆对象,即使是数字、字符串、函数、方法、类、模块

其中和本文关系最大的是,函数也是对象

于是乎一个函数对象就可以用来作为一个“值”来被传递,作为另一个函数的参数、另一个函数的返回值

不过严谨一些来说,其实传递的是函数对象的引用,即没有把这个函数在内存中所占的那块空间的数据复制到零一块空间,而只是一个可以寻找到这块空间的“定位坐标”在被传递,如果你学过C语言,可以把Python的函数对象的引用类比C语言中的函数指针。

注:lambda语句用来构造一个简单的暂时没有名称的函数

现在 上图中的 func_square 和上上图的 add 都是一个函数的引用标识符,你可以将这些标识符传入另一个函数或者从另一个函数中返回出来。

还有,引申一下,标识符不只是指代变量名。变量名、函数名、类名、操作语句(if、while、goto)都是标识符,它是一个更大的概念。

下面看一个函数对象作为参数的例子

返回一个函数对象的引用也是类似的,只需要 return 函数对象标识符 就好

def func():
    # 一些操作构造了一个函数 func_to_return
    
    # 返回这个函数引用
    return func_to_return

这里有一个点值得注意,函数标识符只是代表这函数的一个引用,在传递的时候要传递引用,而不要在函数标识符后面加括号,否则就成了执行这个函数获取函数返回值了。

装饰器的本质

好了,铺垫够了,让我们来开始看看Python的装饰器到底是什么。

装饰器用于你想给一些函数快速添加一些功能的时候,比如你希望每一个函数在执行开始和结束的的时候都打印输出自己的信息来方便你调试。

现在你有一个普通的函数

你希望它在函数开始的时候和结束的时候分别都能输出 myFunc start 和 myFunc end 来通知你,于是你可能会这么做

然而要使用这样的办法的话,如果我们有很多函数,我们不得不为每一个函数手动添加很多次打印语句

在编程中,一旦发现需要重复的操作,我们就应该把这些操作写成一个函数以便之后来复用。

如上图,我们把需要重复的操作做成了一个函数,然后改变我们的调用方式,在原本要调用func()的地方改成调用print_start_and_end(),其中把我们想要附加功能的函数作为参数传入print_start_and_end(),这样就间接的达到了我们的目的。

然而难不成所有的地方都得通过调用print_start_and_end(func)的方式来达到我们的目的?这样的话整篇全是print_start_and_end,会让人发疯的……

我们可以通过下图这样通过修改原函数为一个添加了新功能的函数来实现,其本质是函数的重定义

通过重新定义每个函数后,我们就不用去看满屏幕的 print_start_and_end(...) 了。

然而事情还没完,难不成我们要专门在一个地方把所有的函数都重新定义一遍,就像下边代码一样?

# 此模块要在所有模块之前执行,目的是为了重新定义所有的函数以便于为所有函数添加功能

myFunc_1 = print_start_and_end(myFunc_1)
myFunc_2 = print_start_and_end(myFunc_2)
myFunc_3 = print_start_and_end(myFunc_3)
myFunc_4 = print_start_and_end(myFunc_4)
myFunc_5 = print_start_and_end(myFunc_5)
myFunc_6 = print_start_and_end(myFunc_6)
myFunc_7 = print_start_and_end(myFunc_7)
myFunc_8 = print_start_and_end(myFunc_8)

# ......

显然这个方法太蠢了,不用担心,Python帮我们为这个工作做了语法上的优化(即语法糖),我们可以通过在我们要添加功能的的函数定义前面加上 @print_start_and_end ,python编译器就会自动帮我们做上面的工作。

有一点要注意,装饰一个函数的时候是 @print_start_and_end , 而不是 @print_start_and_end(),前者是装饰器的引用,后者则执行了这个函数,返回了这个函数要返回的一个“值”,那样就成了对于这个返回值的引用了,而如果这个函数值不是一个装饰器的话,就会报错。

完美!这就是Python中装饰器的本质。

带参数的装饰器

不过问题来了,如果我们要装饰(添加功能)的函数本身带参数怎么办呢?

上图中我们没有考虑到这个情况,下面我们来看如何构造一个可以传入参数的装饰器函数。

在研究带参数的装饰器之前,我们先讨论一个小知识点来做一下铺垫,我们来研究一个简单的例子

上图中,3是一个int类型的对象哦,和C语言的int不一样的,C语言中 int a = 3; 传递 a 就是存粹的传递值(传递3这个数字)

而在Python这样的纯面向对象的语言中,传递 a 其实传递的是 int 这个对象的引用,(或许这个引用指向的对象可能有一个属性是数值 3 ?)

而Python、Java这些可以全自动回收使用完毕的内存(内存垃圾)的语言,都是通过一种叫做“引用计数”的方法,来判断什么时候该自动释放掉这些不用的内存的(垃圾自动回收)。

原理说来也简单,每一个被创建的对象都有一个地方在记录着这个对象被引用了多少次。

某个对象刚刚被创建的时候其引用一般是 1 次,因为我们一般会立刻用一个标识符来引用这个对象。

下图中那个int类型的对象被引用了 3 次。

当调用上图中的 func() 函数的时候,会创建一个int对象,并且创建 3 个对于这个对象的引用,当函数退出时,这三个变量的作用域结束,3个引用被销毁,这时,Python的垃圾回收器会发现那个被引用次数为 0 的int对象,便会自动释放掉那个对象所占有的内存,至此,那个int对象被销毁。

好吧,上边那个例子很简单,但是下边这个例子就不简单了

在函数 func 中,我们仅仅是返回了那个在函数内定义的 func_to_return,可没有返回 变量 a,也就是说现在在外面接受 func 函数返回的函数的标识符 myFunc 仅仅是对于 func_to_return 函数对象的引用,那么问题来了 print(a) 语句中的 a = 3 从哪来的?

结合上面谈到的Python垃圾回收机制就不难理解了,尽管 func 函数执行结束了,可由于 func_to_return 指向的函数对象仍然被 myFunc 标识符引用着,这个函数对象里的任何引用的子对象都仍然被引用着,所以其空间也不能释放,我们依然可以使用 func_to_return 函数中引用的对象,而不用担心这个函数引用了函数外的对象在脱离原来上下文的情况下会执行出现问题。

我们管Python中这个现象叫做闭包

做完这个小小的铺垫,我们就可以直接来理解带参数的装饰器的构造了。

带参数的装饰器本质不是一个类似于上面的装饰器,而是一个装饰器生成函数。

执行它将返回一个正常的装饰器,其中这个装饰器引用了了装饰器生成函数中对象,这个对象一般是我们传入的参数,在下图这个装饰器生成函数中,这个对象是 should_remind_after_func_end 指向的 bool 类型对象。

这样一来,我们只需要执行以下这个装饰器生成函数,在执行的时候顺便传入一些参数,这样这个装饰器生成函数就能返回一个引用过我们作为参数传入的对象的装饰器了(有点复杂,慢慢理解)。

如下图那样,装饰器本质还是重新定义了被装饰的函数,只不过现在装饰器中包含了我们传入的参数。

当然,还是利用Python给我们的装饰器语法糖,我们可以像下面这么写。

注意此时装饰器就要带括号了哦,因为我们这里在@后面本质上来说应该填一个装饰器,而我们填了一个装饰器生成函数,一定要加括号来执行这个装饰器生成函数才能返回一个装饰器,(而且如果不加括号我们的参数该往哪里填?)。

上面的装饰器生成函数我们使用了闭包的概念(返回的函数对象引用了函数定义外面对象,则外面那个被引用变量不会随着装饰器生成函数的结束而释放)

下边我们还可以不用闭包的概念也能实现装饰器生成函数,而且貌似更好理解。

我们只需要根据传入的参数生成不同的生成器就好。

总结

  • Python中函数也是一个对象,可以被作为参数值、返回值来传递
  • 装饰器本质是对于原有函数的重定义,重定义为一个新函数,这个新函数被装饰器函数添加了一些新功能
  • 带参数的装饰器本质是一个装饰器生成函数,执行它返回一个装饰器。

好啦,完结撒花!。

发布了1 篇原创文章 · 获赞 2 · 访问量 101

猜你喜欢

转载自blog.csdn.net/Rabbit_Gray/article/details/104067732