Python装饰器核心原理与实战:从基础到高阶应用
本文深入解析Python装饰器的实现原理,涵盖函数名机制、高阶函数与嵌套函数的应用。详细演示如何编写无参、带参及多重装饰器,重点讲解参数与返回值的透明化处理技巧,帮助开发者在不修改原函数源码的前提下灵活扩展功能。
Python装饰器核心原理与实战:从基础到高阶应用
什么是装饰器
装饰器的定义与核心价值
在软件产品迭代升级过程中,经常需要为现有函数增加新功能。由于项目中相同的函数可能被调用上百次,如果逐一修改源码,工作量巨大且容易出错(例如新增功能涉及参数或返回值变更时)。此时,装饰器便展现出其核心价值:它能够在不改变原函数调用形式(即保持函数透明性)的前提下,为函数动态添加新功能。下文将详细阐述其实现方式及底层原理。
装饰器遵循的核心原则
装饰器遵循的原则
顾名思义,装饰器的作用是“装饰”,其核心原则是:被装饰的对象在外部表现上应保持不变,即不能改变被装饰函数的源代码。这是编写装饰器必须遵守的铁律。为了遵循这一原则,我们需要先理解三个基础概念。
函数名即变量
函数名即“变量”
在Python中,函数名类似于C语言中的函数指针,它代表函数的内存地址。只有解释器获取到该地址,才会执行对应的内存代码。因此,从本质上看,函数名与普通变量没有区别,仅在于它们所指向内存的使用方式不同。这是由底层解释器机制决定的,对开发者透明,故可认为两者在变量属性上等同。
高阶函数的判定
高阶函数
判断一个函数是否为高阶函数,只需把握两个原则:
- 形式参数包含函数名
- 返回值包含函数名
只要满足上述任一条件,即可称为高阶函数。回顾前文,这里将函数名作为实参传递,正是高阶函数的典型应用。
嵌套函数的定义
嵌套函数
嵌套函数的定义同样简单,只需把握一个原则:
- 在一个函数的函数体内定义另一个函数
需要强调的是,函数定义时并不会执行其函数体,这与定义变量时不读取变量内容同理。这一特性对于理解装饰器的实现原理至关重要。
装饰器的设计与实现步骤
如何写装饰器
基于上述铺垫,现在详细解析装饰器的编写方法,这将使理解变得更加直观。
装饰器的本质解析
装饰器本质
装饰器本质上是一个函数,具备函数名、参数和返回值。在Python中,我们通常使用“@auth”语法糖来表示装饰器。
@auth # 其等价于:func = auth(func)
def func():
print("func called")
此示例展示了Python中修饰func函数的格式,尽管尚未实现具体的装饰器函数。需注意注释中的内容,我们可以得出以下结论:
- 装饰器函数实际上是一个高阶函数(其参数和返回值均为函数名)。
- “auth(func)”是在调用装饰器函数,这意味着装饰器的函数体会立即执行,这一点务必牢记。
装饰器的设计思路
设计思路
既然装饰器是一个函数,且具备上述等价关系,我们可以按以下思路设计:
- 在装饰器的函数体内定义一个新函数,在该新函数内部调用被修饰的函数,并在其前后添加新功能。最后,利用装饰器函数的返回值返回新定义函数的函数名。
- 由此可知,“func = auth(func)”中的返回值func,实际上表示的是在装饰器中新定义的函数的函数名。
装饰器的底层机制
前文的大量铺垫旨在揭示装饰器的实现机制,其实并不复杂:
- 装饰器机制改变了被修饰函数名所指向的内存地址。简而言之,修饰前,函数名指向内存块A;修饰后,函数名指向内存块B。但在执行内存块B时,会调用内存块A。内存块B中的代码即为我们新增的功能。这种机制通过“高阶函数”和“嵌套函数”实现。
- 最终效果是,当调用被修饰后的函数时,实际调用的是装饰器新申请的内存块(即内存块B),而非原来的内存块。
无参装饰器的编写实战
第一步:设计装饰器函数
装饰器函数的定义与普通函数无异,关键在于函数体的编写。此处为便于理解,先以无参数装饰器为例进行说明。
#装饰器函数定义格式
def deco(func):
'''函数体...'''
return func
这里的“无参数”指除了“func”外没有其他参数。难点在于函数体的编写,以下示例解释了为何需要第二步:
#使用语法糖@来装饰函数,相当于“myfunc = deco(myfunc)”
def deco(func):
print("before myfunc() called.")
func()
print("after myfunc() called.")
return func
@deco
def myfunc():
print("myfunc() called.")
myfunc()
myfunc()
#output:
before myfunc() called.
myfunc() called.
after myfunc() called.
myfunc() called.
myfunc() called.
从输出结果可见,装饰器并未按预期生效。请勿误以为装饰器只生效了一次,这是因为忽略了“@deco”的等效机制。当解释到“@deco”时,等价于执行“myfunc = deco(myfunc)”。注意,此处正在调用deco函数,因此deco的函数体会被执行。所以,输出前三行并非调用myfunc时的效果,这不能说明装饰器生效了一次。第二步旨在解决装饰器看似未生效的问题。
包装被修饰函数
第二步:包装被修饰函数
#基本格式
def deco(func):
def _deco()
#新增功能
#...
#...
func() #别修饰函数调用
return_deco
以下给出一个示例:
#使用内嵌包装函数来确保每次新函数都被调用,
#内嵌包装函数的形参和返回值与原函数相同,装饰函数返回内嵌包装函数对象
def deco(func):
def _deco():
print("before myfunc() called.")
func()
print("after myfunc() called.")
# 不需要返回func,实际上应返回原函数的返回值
return _deco
@deco
def myfunc():
print("myfunc() called.")
return 'ok'
myfunc()
#output:
before myfunc() called.
myfunc() called.
after myfunc() called.
参数与返回值的透明化处理
第三步:被修饰函数参数和返回值透明化处理
完成第二步后,装饰器已具备主要功能,接下来需处理被修饰函数的参数和返回值,以真正实现装饰器的铁律。直接看代码:
#基本格式
def deco(func):
def _deco(*args, **kwargs) #参数透明化
#新增功能
#...
#...
res = func(*args, **kwargs) #别修饰函数调用
return res #返回值透明化
return_deco
通过上述分析可知:
参数透明化:调用被装饰后的函数时,实际调用的是这里的_deco函数。因此,给_deco函数加上可变参数,并将这些参数传递给func函数即可。返回值透明化:原理同参数透明化,给_deco函数定义返回值,并返回func的返回值即可。
透明化处理如此简单!至此,我们的装饰器编写完成。以下是一个示例:
#对带参数的函数进行装饰,
#内嵌包装函数的形参和返回值与原函数相同,装饰函数返回内嵌包装函数对象
def deco(func):
def _deco(*agrs, **kwagrs):
print("before myfunc() called.")
ret = func(*agrs, **kwagrs)
print(" after myfunc() called. result: %s" % ret)
return ret
return _deco
@deco
def myfunc(a, b):
print(" myfunc(%s,%s) called." % (a, b))
return a + b
print("sum=",myfunc(1, 2))
print("sum=",myfunc(3, 4))
#output:
before myfunc() called.
myfunc(1,2) called.
after myfunc() called. result: 3
sum= 3
before myfunc() called.
myfunc(3,4) called.
after myfunc() called. result: 7
sum= 7
装饰器进阶应用
装饰器进阶
带参数装饰器的实现
带参数装饰器
既然装饰器也是函数,自然可以传递参数。这里指的是“@auth(auth_type = 'type1')”这种形式。先上代码:
#基本格式
def deco(deco_type)
def _deco(func):
def __deco(*args, **kwargs) #参数透明化
#新增功能
#...
#...
print("deco_type:",deco_type) #使用装饰器参数
res = func(*args, **kwargs) #别修饰函数调用
return res #返回值透明化
return __deco
return_deco
本质上,这是在原有装饰器基础上再外层套一个deco函数,用于接收装饰器参数。由于在最外层套了一个函数,该函数的形参作用域仅限于其函数体内部,因此内部函数定义可直接使用。如何理解解释器的解析过程?只需明白一点:“@auth(auth_type = 'type1')”等价于“func = auth(auth_type = 'type1')(func)”。解释器会先翻译“auth(auth_type = 'type1')”,再将其返回值(假设赋给不存在的函数名_func)作为函数指针,此时_func代表_deco,接着执行“func = _func(func)”,而这里的func代表的其实就是__deco。
至此,实现了通过装饰器传递参数的目的。以下是一个示例:
#示例7: 在示例4的基础上,让装饰器带参数,
#和上一示例相比在外层多了一层包装。
#装饰函数名实际上应更有意义些
def deco(deco_type):
def _deco(func):
def __deco(*args, **kwagrs):
print("before %s called [%s]." % (func.__name__, deco_type))
func(*args, **kwagrs)
print(" after %s called [%s]." % (func.__name__, deco_type))
return __deco
return _deco
@deco("mymodule")
def myfunc():
print(" myfunc() called.")
@deco("module2")
def myfunc2():
print(" myfunc2() called.")
myfunc()
myfunc2()
#output:
before myfunc called [mymodule].
myfunc() called.
after myfunc called [mymodule].
before myfunc2 called [module2].
myfunc2() called.
after myfunc2 called [module2].
多重装饰器的使用
多重装饰器修饰函数
如果理解了前述内容,那么多重装饰器就非常简单了。这相当于对装饰器中被修饰的函数再次进行装饰。换个角度看,关注点放在原被修饰函数上,会发现我们可以为其添加多个功能。以下是一个示例:
def deco(deco_type):
def _deco(func):
def __deco(*args, **kwagrs):
print("before %s called [%s]." % (func.__name__, deco_type))
func(*args, **kwagrs)
print(" after %s called [%s]." % (func.__name__, deco_type))
return __deco
return _deco
@deco("module1")
@deco("mymodule")
def myfunc():
print(" myfunc() called.")
@deco("module2")
def myfunc2():
print(" myfunc2() called.")
myfunc()
#output:
before __deco called [module1].
before myfunc called [mymodule].
myfunc() called.
after myfunc called [mymodule].
after __deco called [module1].
请注意结果,@deco("module1")修饰的是deco("mymdule"),这与我们预期的完全一致,完美运行!
领取干货:零基础入门学习python视频教程
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













