Python装饰器详解:从原理到带参装饰器与属性修复

慢慢来
慢慢来
发布时间:2026-10-14 | 7人浏览过
原创

本文深入解析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 装饰器的资料请关注本站其它相关文章!

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表