Python装饰器
一、函数闭包
- 一个实例:输出0~100之间所有奇数,并统计函数执行时间
def print_odds():
"""
输出0~100之间所有奇数,并统计函数执行时间
"""
start_time = time.clock() # 起始时间
# 查找并输出所有奇数
for i in range(100):
if i % 2 == 1:
print(i)
end_time = time.clock() # 结束时间
print("it takes {} s to find all the olds".format(end_time - start_time))
if __name__ == '__main__':
print_odds()
分析:函数逻辑(查找奇数)和辅助功能(记录时间)耦合在一起了.不方便修改,容易引发bug
- 改进1:将辅助功能(记录时间)抽离成一个辅助函数count_time,在辅助函数count_time中调用主要功能函数print_odds.
def count_time(func):
"""
统计某个函数的运行时间
"""
start_time = time.clock() # 起始时间
func() # 执行函数
end_time = time.clock() # 结束时间
print("it takes {} s to find all the olds".format(end_time - start_time))
def print_odds():
"""
输出0~100之间所有奇数,并统计函数执行时间
"""
for i in range(100):
if i % 2 == 1:
print(i)
if __name__ == '__main__':
# print_odds()
count_time(print_odds)
分析:仍有缺点,客户端需要调用的是print_odds函数,最好不要显式地要求客户端去调用count_time函数
- 改进2:在count_time函数中新定义一个函数,这个函数用于包装print_odds,给其添加计时的功能。count_time返回这个新定义的函数。
- 可以将这个返回的函数赋值给一个print_odds变量从而使其指向被包装后的函数
- 再次调用print_odds时,调用的就是包装好后的函数
def count_time_wrapper(func):
"""
闭包,用于增强函数func: 给函数func增加统计时间的功能
"""
def improved_func():
start_time = time.clock() # 起始时间
func() # 执行函数
end_time = time.clock() # 结束时间
print("it takes {} s to find all the olds".format(end_time - start_time))
return improved_func
if __name__ == '__main__':
# 调用count_time_wrapper增强函数
print_odds = count_time_wrapper(print_odds)
print_odds()# improved
print_odds()# improved
print_odds()# improved
print_odds()# improved
print_odds()# improved
-
函数闭包的概念:
- 函数闭包本质上是一个函数
- 函数闭包传入的参数和返回值都是一个函数
- 函数闭包返回值的函数是对传入参数增强后的结果
- 例如上述改进2中的count_time_wrapper就是一个函数闭包
-
带参数的函数闭包
-
错误的实例
-
def count_time_wrapper(func): """ 闭包,用于增强函数func: 给函数func增加统计时间的功能 """ def improved_func(): start_time = time.clock() # 起始时间 func() # 执行函数 end_time = time.clock() # 结束时间 print("it takes {} s to find all the olds".format(end_time - start_time)) return improved_func def count_odds(lim=100): """ 输出0~lim之间所有奇数,并统计函数执行时间 """ cnt = 0 for i in range(lim): if i % 2 == 1: cnt+=1 return cnt if __name__ == '__main__': print('增强前') print(count_odds(lim=10000)) # 装饰前函数能正常返回,能接收参数 print('----------------------') print('增强后') count_odds = count_time_wrapper(count_odds) print(count_odds(lim=10000)) # 装饰后函数不能正常返回,不能接收参数 -
显然这段程序不能正常执行。由于count_odds实际上是improved_func,没有任何参数
-
简单的修改方法是给improved_func加上与原包装函数相同的参数。但是这样做是不太可取的。应该将improved_func接收到的所有参数传给被包装的函数func。防止参数的遗漏。更改方法如下:
def count_time_wrapper(func): """ 闭包,用于增强函数func: 给函数func增加统计时间的功能 """ def improved_func(*args, **kwargs): # 增强函数应该把就饿收到的所有参数传给原函数 start_time = time.clock() # 起始时间 ret = func(*args, **kwargs) # 执行函数 end_time = time.clock() # 结束时间 print("it takes {} s to find all the olds".format(end_time - start_time)) return ret # 增强函数的返回值应该是原函数的返回值 return improved_func
-
二、语法糖
- 看下面的代码,通过装饰器进行函数增强,只是一种语法糖,本质上跟上个程序完全一致.
def count_time_wrapper(func):
"""
闭包,用于增强函数func: 给函数func增加统计时间的功能
"""
def improved_func():
start_time = time.clock() # 起始时间
func() # 执行函数
end_time = time.clock() # 结束时间
print(
"it takes {} s to find all the olds".format(end_time - start_time))
return improved_func
@count_time_wrapper
def print_odds():
"""
输出0~100之间所有奇数,并统计函数执行时间
"""
for i in range(100):
if i % 2 == 1:
print(i)
if __name__ == '__main__':
print_odds()
上述main函数等价于执行(如果去掉16行的count_time_warpper)
if __name__ == '__main__':
print_odds = count_time_wrapper(print_odds)
print_odds()
-
多个语法糖:将其转化为和函数闭包相同的形式。是很好分析的。
-
def count_time_wrapper(func): """ 闭包,用于增强函数func: 给函数func增加统计时间的功能 """ def improved_func(): start_time = time.clock() # 起始时间 func() # 执行函数 end_time = time.clock() # 结束时间 print("it takes {} s to find all the olds".format(end_time - start_time)) return improved_func def log_wrapper(func): """ 闭包,用于增强函数func: 给func增加日志功能 """ def improved_func(): start_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())) # 起始时间 func() # 执行函数 end_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())) # 结束时间 print("Logging: func:{} runs from {} to {}".format(func.__name__, start_time, end_time)) return improved_func @count_time_wrapper @log_wrapper def count_odds(): """ 输出0~100之间所有奇数,并统计函数执行时间 """ cnt = 0 for i in range(100): if i % 2 == 1: cnt += 1 return cnt if __name__ == '__main__': count_odds() -
上述实例的包装过程如下:
- 先使用log_warpper进行包装,返回一个improved_func,这个返回的实例具有了增强日志的功能。如果此时调用improved_func的话那么就会在执行count_odds函数的基础上打印出日志。但注意此时我们并没有调用这个improved_func
- 再使用count_time_wrapper对返回的improved_func进行包装。也就是说count_time_warpper函数的func参数是这个improved_func(刚刚Log_warpper的返回值),函数执行后返回一个函数,这个函数对应于count_time_warpper里面的improved_func,这就是最后在主函数中调用count_odds所应该执行的函数
- 根据以上的分析,很容易得出程序的输出顺序如下:打印0-100中所有的奇数->打印logging日志->打印计时信息
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TXk843Il-1629372110030)(C:\Users\nth12\AppData\Roaming\Typora\typora-user-images\image-20210810120059055.png)]
-
三、利用装饰器语法糖构造多种不同类型的装饰器
3.1 基础装饰器
-
需要打印两个数的和。在该函数的开始和结束需要打印日志。打印日志由装饰器实现
-
#装饰器函数 def logger(func): def wrapper(*args, **kw): print('我准备开始执行:{} 函数了:'.format(func.__name__)) # 真正执行的是这行。 func(*args, **kw) print('执行完啦。') return wrapper @logger def add(x, y): print('{} + {} = {}'.format(x, y, x+y)) -
如果此时调用add(20,5)的话,由第二章的分析,我们可知实际上的执行是如下的顺序:
-
add = logger(add) add(20,5) -
结果是打印我准备开始执行,然后打印两数相加的结果,最后打印执行完毕的结果。第一行语句返回包装后的函数warpper,注意这里的warpper会有两个参数*args和**kw。*args会接收所有的非a = 参数值形式的参数;**kw会以字典形式接受 a = 参数值形式的参数。因此这里传入的20,5都被送入args(元组)中,没有东西被传入字典中
-
warpper函数(实际上是add函数)在执行过程中调用func也就是原先Logger中传入的add函数。*args表示对于args进行解包,**kw也是一样的。有关Python中*用法的更多细节,请参考python中*的多种用法
-
3.2 带参数的函数装饰器
-
来看下面一个代码实例。
-
def say_hello(contry): def wrapper(func): def deco(*args, **kwargs): if contry == "china": print("你好!") elif contry == "america": print('hello.') else: return # 真正执行函数的地方 return func(*args, **kwargs) return deco return wrapper @say_hello("china") def xiaoming(): pass @say_hello("america") def jack(): pass -
如果此时执行下述调用
-
xiaoming() -
那么主程序会先打印出你好。这等价于下面的函数闭包形式:
-
xiaoming = sayhello('china')(xiaoming) xiaoming() """ 等价于下面的代码形式: wrapper = sayhello('china') #即sayhello函数的返回值warpper,sayhello函数接收装饰器语法糖中的参数'china' xiaoming = wrapper(xiaoming) #warpper函数的参数为原始未被装饰的函数。在warpper里面新定义了一个函数deco,这个函数除了调用warpper的参数外(即原始未被装饰的函数),还根据say_hello接收到的参数做了一些额外的操作。由于warpper函数返回了这个deco对象,因此现在xiaoming实际上就是deco; xiaoming() #实际调用的是deco,这里比较特殊,xiaoming没有任何参数。如果xiaoming有参数,调用时直接传入参数即可。 """ -
对上面的函数闭包形式进行分析:
- 首先,函数say_hello接收装饰器字符串,返回一个warpper函数对象。
- warpper函数对象的参数为被装饰的函数,在此例中为xiaoming
- warpper被调用后返回一个deco函数对象,这个函数对象即为经过装饰以后的xiaoming函数对象,由warpper函数被返回。
- 最后实际调用的是deco函数对象。
-
3.3 类装饰器
3.3.1 不带参数的类装饰器
-
基于类装饰器的实现,必须实现
__call__和__init__两个内置函数。__init__:接收被装饰函数__call__:实现装饰逻辑。 -
看下面这个实例:
-
class logger(object): def __init__(self, func): self.func = func def __call__(self, *args, **kwargs): print("[INFO]: the function {func}() is running..."\ .format(func=self.func.__name__)) return self.func(*args, **kwargs) @logger def say(something): print("say {}!".format(something)) say("hello") -
打印输出INFO信息,再打印出say函数中的信息。
-
该种装饰器的执行过程相当于执行下面的函数闭包形式:
-
say = logger(say) say(something) -
分析:
- 函数的一个行语句调用logger类的构造函数,以原始函数say为参数构造一个logger类的具体对象
- 这时say是一个logger对象,由于logger对象都有
_call_方法,因此第二行的say相当于调用自己的call方法。
-
3.3.2 带参数的类装饰器
-
这种带参数的类装饰器的工作机理与3.1中讲述的装饰器的工作原理是类似的。
-
看下面的示例代码:
-
class logger(object): def __init__(self, level='INFO'): self.level = level def __call__(self, func): # 接受函数 def wrapper(*args, **kwargs): print("[{level}]: the function {func}() is running..."\ .format(level=self.level, func=func.__name__)) func(*args, **kwargs) return wrapper #返回函数 @logger(level='WARNING') def say(something): print("say {}!".format(something)) say("hello") -
先输出log信息,再输出say信息。等价于下面的函数闭包形式
-
a = logger('WARNING') say = a(say) say('hello') -
分析
- 第一行实例化一个logger对象a,参数为装饰器语法糖中给定的参数WARNNING;
- 第二行相当于执行a对象的
_call_方法,定义一个warpper函数并返回这个函数;此时的say就是返回的wrapper - 最后调用say,实际上调用的是返回的warpper;
-
3.4 能够装饰类的装饰器
-
实际上是对类的生成过程加以控制。
-
看下面的实例代码:
-
instances = {} def singleton(cls):#cls代表一个类 def get_instance(*args, **kw): cls_name = cls.__name__ #获取类名 print('===== 1 ====') if not cls_name in instances: print('===== 2 ====') instance = cls(*args, **kw) instances[cls_name] = instance return instances[cls_name] return get_instance @singleton class User: _instance = None def __init__(self, name): print('===== 3 ====') self.name = name User('abc') -
和之前的情况类似,只不过singletion函数中的装饰器换成了一个类。
-
3.5 property装饰器
-
Motivation:观察下面的代码示例
class Student(object): def __init__(self, name, age=None): self.name = name self.age = age # 实例化 xiaoming = Student("小明") # 添加属性 xiaoming.age=25 # 查询属性 xiaoming.age # 删除属性 del xiaoming.age- 上述代码的健壮性是非常差的。不合法的age输入无法检测。
- 可以考虑将字段设为私有,同时增加set_age和get_age函数,在里面检测异常的发生。
- 但是我们更希望直接按照上述方法,但又能使程序检测我们输入age的合法性。这就需要用到property装饰器了。
-
定义:
class `property`(fget=None, fset=None, fdel=None, doc=None)- fget 是获取属性值的函数。 fset 是用于设置属性值的函数。 fdel 是用于删除属性值的函数。并且 doc 为属性对象创建文档字符串。
-
使用:看如下的例子
-
典型用法:
-
class C: def __init__(self): self._x = None def getx(self): return self._x def setx(self, value): self._x = value def delx(self): del self._x x = property(getx, setx, delx, "I'm the 'x' property.") #如果 c 是 C 的实例,c.x 将调用getter,c.x = value 将调用setter, del c.x 将调用deleter。 -
装饰器用法:
-
class C: def __init__(self): self._x = None @property def x(self): """I'm the 'x' property.""" return self._x @x.setter def x(self, value): self._x = value @x.deleter def x(self): del self._x #如果 c 是 C 的实例,c.x 将调用getter,c.x = value 将调用setter, del c.x 将调用deleter。 -
装饰器用法:
-
class C: def __init__(self): self._x = None @property def x(self): """I'm the 'x' property.""" return self._x @x.setter def x(self, value): self._x = value @x.deleter def x(self): del self._x #如果 c 是 C 的实例,c.x 将调用getter,c.x = value 将调用setter, del c.x 将调用deleter。
-
-
底层实现
本文详细探讨了Python装饰器的实现原理,从函数闭包开始,逐步改进,实现了统计执行时间和日志记录的功能。通过实例展示了如何使用装饰器语法糖,并解释了带参数的装饰器和类装饰器的用法。此外,还介绍了property装饰器的应用,用于增强类属性的访问控制。通过对装饰器的深入理解,可以更好地进行函数和类的扩展与优化。
17万+

被折叠的 条评论
为什么被折叠?



