Python装饰器详解:从原理到带参装饰器与属性修复
本文深入解析Python装饰器的核心原理,通过逐步演进的代码示例,讲解如何从基础函数包装过渡到使用柯里化技术处理参数。重点剖析了装饰器对原函数__name__和__doc__属性的影响,并提供了两种修复方案:手动复制属性与将属性复制逻辑封装为装饰器。最后介绍了带参装饰器的实现方式,帮助开发者掌握高阶函数应用。
Python装饰器基础概念
一、 什么是装饰器
正如知乎上某位技术专家所比喻的那样:内裤的主要功能是遮羞,但在寒冷的冬天无法提供足够的保暖。于是人们发明了长裤,它在保留内裤原有功能的基础上,额外提供了御寒效果。在Python中,装饰器就像这条长裤,它在不改变原函数核心逻辑的前提下,为函数增加了额外的功能。装饰器本质上是一个Python函数,它允许开发者在不修改原函数代码的情况下,动态地增加功能。此外,装饰器还能将通用的逻辑(如日志记录、性能监控、事务处理、缓存机制或权限校验)抽离出来,实现代码的复用。
换言之
装饰器的设计原则是不改变原函数的业务逻辑,它本身是一个独立的函数模块。其调用机制遵循“谁调用,谁使用”的原则,即通过特定的语法或调用方式,将装饰器的功能注入到目标函数中。
装饰器示例与参数处理
二、 举个栗子
假设有一个名为add的功能函数,用于计算x和y的和。现在我们需要在执行add函数的同时,记录一些额外的信息(如日志),这部分增强逻辑被称为“装饰”。在装饰函数logger中,我们可以封装各种类似的功能模块,并对它们进行包装和复用,从而实现功能的模块化扩展。
1. 引子
#功能函数
def add(x,y):
return x+y
#装饰函数
def logger(fn):
print('frist')
x = fn(4,5)
print('second')
return x
print(logger(add))
#把函数add传给logger ,return x+y
#print('frist')
#print('secend')
# x = fn(4,5) ==> x = 4 y= 5 x= 4+5 = 9
#return 9
运行结果依次打印: first second 9
2. 提取参数
在上述基础示例中,add函数的参数x和y被硬编码在logger函数内部,这严重限制了函数的灵活性。为了解决这个问题,我们需要将参数提取出来,使装饰器能够接收任意参数。
def add(x,y):
return x + y
def logger(fn,*args,**kwargs):
print('frist')
x = fn(*args,**kwargs)
print('second')
return x
print(logger(add,1,y=11))
运行结果依次打印: first second 12
3. 柯里化
def add(x,y):
return x + y
def logger(fn):
def wrapper(*args,**kwargs):
print('begin')
x = fn(*args,**kwargs)
print('end')
return x
return wrapper
print(logger(add)(5,y=11))
运行结果依次打印: begin end 16
柯里化与函数调用机制
懵逼ing
以下内容为个人理解,左侧展示的是非柯里化函数的调用方式,右侧则是经过柯里化处理后的函数形式。

柯里化函数
前文提到柯里化的定义:原本可以一次性传入多个参数,经过柯里化改造后,调用过程被拆分。左侧示例中,我们需要同时传入add函数和两个参数;而在右侧的柯里化形式中,logger(add)本身返回的是一个函数,我们只需要后续传入两个参数即可。logger(add)作为一个整体,结合成了一个新的函数对象。当然,这种写法使得函数主体的结构发生了变化。 回顾函数传参的基础知识,函数的传参必须与参数定义保持一致。重点分析右侧的柯里化函数: 在参数部分,调用方式分为两步。首先,logger函数接收fn作为参数;其次,logger返回wrapper函数,而wrapper函数接受(*args, **kwargs)。因此,参数需要分两次传入:第一次传入fn,第二次传入wrapper所需的实际参数。这就解释了为何最终调用形式为print(logger(add)(5, y=50))。
在返回值部分,右侧的logger函数是一个嵌套函数结构。logger的返回值是wrapper,而内层wrapper函数的返回值是x,其中x = fn(*args, **kwargs)。这里的fn指的是最后调用时传入的add函数。
懵逼 X 2。。。。
def add(x,y):
return x + y
def logger(fn,*args,**kwargs): def logger(fn): #参数剥离
def newfunction(*args,**kwargs): #新定义一个函数,logger函数返回也是这个函数名字
print('frist') print('frist')
x = fn(*args,**kwargs) == > x = fn(*args,**kwargs)
print('second') print('second')
return x return x
return newfunction
print(logger(add,1,y=11)) print(logger(add)(5,y=11)) #两次传入参数
效果如下:
def add(x,y):
return x + y
def logger(fn): #参数剥离
def newfunction(*args,**kwargs): #新定义一个函数,logger函数返回也是这个函数名字
print('frist')
x = fn(*args,**kwargs)
print('second')
return x
return newfunction
print(logger(add)(5,y=11)) #两次传入参数
运行结果依次打印: first second 16
如果继续感到困惑,可以暂时按照这种方式使用。随着练习增多,自然会领悟其精髓。
语法糖与复杂应用场景
4. 装饰器语法糖
#再次变形。。。
def add(x,y):
return x + y
def logger(fn):
def wrapper(*args,**kwargs):
print('begin')
x = fn(*args,**kwargs)
print('end')
return x
return wrapper
##调用方法1:
print(logger(add)(x=1111,y=1))
##调用方法2:
add = logger(add)
print(add(x=11,y=3))
##调用方法3: python给我们的语法糖
@logger # 说明下边的函数,add 其实是 add = logger(add)
def add(x,y):
return x + y
print(add(45,40))
运行结果依次打印: begin end 1112 begin end 14 begin end 85
三、 复杂的栗子
import datetime
import time
def logger(fn):
def warp(*arges,**kwarges):
print("arges={},kwarges={}".format(arges,kwarges)) #打印函数的两个参数
start = datetime.datetime.now() #获取函数运行的开始时间
ret = fn(*arges,**kwarges) #传入两个参数,调用add函数 此处有个return的值,需要一层一层的返回出去
duratime = datetime.datetime.now() - start #获得函数的运行时间
print("function {} took {}s".format(fn.__name__,duratime.total_seconds())) #打印函数的运行时间
return ret #返回fn的结果 ,fn = x+y ==> 返回x+y的值。 x = 4 y= 11 ==> return 11
return warp #返回warp的 return ==> ret 的return ==> return 11 函数的最终结果为11
@logger
def add(x,y):
print("oooooook")
time.sleep(1.5)
return x+y
print(add(4,y=11))
#如果充分理解了每个小部件,这个简单的完整版本也是很好理解的了。
#1,logger是个装饰器,而且使用了柯里化技术
#2,add 传参给logger的fn 形参,add(4,y=5)的两个参数传入给warp函数的两个形参
#
#
arges=(4,),kwarges={'y': 11}
oooooook
function add took 1.5017s
15
再次翻译
import datetime
import time
#####################################装饰开始############################################
def logger(fn): #拿到函数名称
def warp(*arges,**kwarges): #拿到函数带过来的参数开始装饰
print("arges={},kwarges={}".format(arges,kwarges)) #来试试打印两个参数
start = datetime.datetime.now() #
ret = fn(*arges,**kwarges) # 此处调用add函数。开始执行函数,发现return语句。。ret的结果就是return。
duratime = datetime.datetime.now() - start #
print("function {} took {}s".format(fn.__name__,duratime.total_seconds()))
return ret #加工完成开始返回。warp的返回值是ret ,ret的返回值是 add函数的执行结果(原函数的功能完整的保留了)
return warp # logger的返回结果是warp,warp的返回值是ret ,ret的返回值是 add函数的执行结果(原函数的功能完整的保留了)
#####################################装饰完成############################################
@logger #装饰工厂
######add是需要被装饰的函数,当你有这个想法的事情,其实事情已经开始发生了。
def add(x,y): # 此时add = logger(add) 此处前面的@logger标记就是想要让logger装饰器像一个工厂一样对add函数进行加工。
print("oooooook")
time.sleep(1.5)
return x+y
print(add(4,y=11))
arges=(4,),kwarges={'y': 11}
oooooook
function add took 1.501604s
15
装饰器的副作用与属性修复方案
四、 带参装饰器
1. 文档字符串
我们约定,在Python函数的第一行应包含对函数的说明,通常使用三引号表示。 如果是英文说明,惯例要求首字母大写,第一行写概述,空一行后,第三行写详细描述。 如果函数中包含文档字符串,它默认会存储在函数的__doc__属性中,可以直接通过该属性访问。
def add(x,y):
"""This is a function of addition"""
a = x+y
return x + y
print("function name is {}
function doc = {}
".format(add.__name__, add.__doc__))
print(help(add))
function name is add
function doc = This is a function of addition
Help on function add in module __main__:
add(x, y)
This is a function of addition
None
2. 前面装饰器的副作用
前面的装饰器示例基本上已经能够完成对函数进行加强的功能,但仍存在一些瑕疵。例如,原函数的属性(如__name__和__doc__)已经被替换为装饰器内部wrapper函数的属性。如下所示:
def add(x,y):
return x + y
def logger(fn):
"This is logger doc"
def wrapper(*args,**kwargs):
"This is wrapper doc"
print('begin')
x = fn(*args,**kwargs)
print('end')
return x
return wrapper
@logger # add = logger(add)
def add(x,y):
"This is add doc "
print("name = {}
doc = {}".format(add.__name__,add.__doc__))
return x + y
print(add(45,40))
#可以看出来add被装饰出来的函数(新的add)的属性已经全部改变了。
运行结果: name = wrapper doc = This is wrapper doc 85
3. 解决方案一
涉及三个函数:
第一个:copy原函数的属性 copy_properties 第二个:装饰器 logger 第三个:功能函数 add
def copy_properties(src, dst): # 把src的相关属性赋值给dst (fn,wrap)
dst.__name__ = src.__name__
dst.__doc__ = src.__doc__
def logger(fn):
"""'This is a function of logger'"""
def wrap(*arges,**kwarges): #
"""'This is a function of wrap'"""
print('<>-<>-<>-<>-<>-<>-<>-<>-<>-<>-<>-<>-<>-<>-<>-<>-<>-<>')
x = fn(*arges,**kwarges)
#print("name={}
doc={}".format(add.__name__,add.__doc__))
print('<>-<>-<>-<>-<>-<>-<>-<>-<>-<>-<>-<>-<>-<>-<>-<>-<>-<>')
return x
copy_properties(fn,wrap) #思考1:为什么放在这个位置调用
return wrap
@logger
def add(x,y):
"""'This is a function of add'"""
print("name={}
doc={}".format(add.__name__,add.__doc__))
return x+y
print(add(4,6))
运行结果: <>-<>-<>-<>-<>-<>-<>-<>-<>-<>-<>-<>-<>-<>-<>-<>-<>-<> name=add doc='This is a function of add' <>-<>-<>-<>-<>-<>-<>-<>-<>-<>-<>-<>-<>-<>-<>-<>-<>-<> 10
4. 解决方案二
既然使用装饰器都会出现属性丢失的问题,为什么不把copy_properties也做成一个装饰器呢?
涉及三个函数:
第一个:copy原函数的装饰器 copy_properties1 第二个:装饰器 logger 第三个:功能函数 add
def copy_properties(src, dst): # 把src的相关属性赋值给dst (fn,wrap)
dst.__name__ = src.__name__
dst.__doc__ = src.__doc__
#利用前面的知识我们可以对copy_properties轻松进行变形
def copy_properties1(src): # 把src的相关属性赋值给dst (fn,wrap)
def _copy(dst):
dst.__name__ = src.__name__
dst.__doc__ = src.__doc__
return dst
return _copy
带参装饰器实现
带参装饰器:
def logger(fn):
"""'This is a function of logger'"""
@copy_properties1(fn) #wrap = copy_properties(fn)(wrap)
#== > 柯里化 两次传入参数 src = fn , dst = wrap 新的wrap函数的属性已经替换为原函数的。
def wrap(*arges,**kwarges): #wrap = copy_properties(fn)(wrap)(*arges,**kwarges)
"""'This is a function of wrap'"""
print('>->->->->->->->->->->->->->->->->->->->->->->->->->')
x = fn(*arges,**kwarges)
print('<-<-<-<-<-<-<-<-<-<-<-<-<-<-<-<-<-<-<-<-<-<-<-<-<-<')
return x
return wrap
@logger #add =logger(add)
def add(x,y):
"""'This is a function of add'"""
print("name={}
doc={}".format(add.__name__,add.__doc__))
return x+y
print(add(4,11))
以上就是详解Python 装饰器的详细内容,更多关于Python 装饰器的资料请关注本站其它相关文章!
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









