Python 装饰器的本质就是函数闭包的语法糖。
一、什么是函数闭包
函数闭包:一个函数,其参数和返回值都是函数,其作用简单概括有以下两点:
- 用于增强函数功能
- 面向切面编程(AOP) 例如,现在需要写一个函数,该函数用来计算 0 ~ 100 之内整数之和,并同时输出执行时间,功能比较简单,只需要通过 for 循环求和并在 for 循环前后获取一下时间等 for 循环执行完后相减就可以知道执行时间,因此,比较简单的实现方式如下:
def cal_sum():
start_time = datetime.now()
sum = 0
for i in range(0, 101):
sum = sum + i
end_time = datetime.now()
print('执行耗时为:{},计算结果为:{}'.format(end_time - start_time, sum))
cal_sum()
上述代码可以发现,cal_sum 函数做了两件事情,1)记录执行时间,2)计算 0 ~ 100 之间整数之和,相当于将两个功能耦合在一块,这样就会可读性比较差,且这种写法不方便修改,容易引起 bug,因此,我们可以针对上述的代码,拆分成两个函数,一个函数负责记录执行时间,另一个函数负责求和:
def time_print(func):
start_time = datetime.now()
func()
end_time = datetime.now()
print('执行耗时为:{}'.format(end_time - start_time))
def cal_sum():
sum = 0
for i in range(0, 101):
sum = sum + i
print('计算结果为:{}'.format(sum))
time_print(cal_sum)
上述代码可以达到解耦的目的,但是在调用过程中发现这里实际上是在 time_print 函数去调用了 cal_sum 函数,这种写法可能不太好区分其实现的主要功能,因此,还可以对上述代码进行优化:
def time_print(func):
def deco():
start_time = datetime.now()
func()
end_time = datetime.now()
print('执行耗时为:{}'.format(end_time - start_time))
return deco
def cal_sum():
sum = 0
for i in range(0, 101):
sum = sum + i
print('计算结果为:{}'.format(sum))
print_sum = time_print(cal_sum)
print_sum()
上述代码可以实现在调用主要功能函数时,自动完成了时间的统计的功能,其中,time_print 就是所谓的函数闭包了,它的功能是用于增强输入的 func 函数,给 func 函数增加统计时间的功能,其中,deso 就是增强后的 func,通过 print_sum = time_print(cal_sum),可以实现对 cal_sum 函数的增强,返回值是一个增强后的函数(类似于工厂方法)。
注意:其实有一种机制可以让 python 解释器在第一次调用增强函数时,自动调用闭包函数对原函数进行增强,而不需要显式调用闭包函数,这个机制就是装饰器。
二、装饰器的本质
1)装饰器本质上是对函数闭包的语法糖
2)装饰器在第一次调用被装饰的函数时调用闭包进行函数增强
通过装饰器形式改一下上述的代码:
def time_print(func):
def deco():
start_time = datetime.now()
func()
end_time = datetime.now()
print('执行耗时为:{}'.format(end_time - start_time))
return deco
@time_print
def cal_sum():
sum = 0
for i in range(0, 101):
sum = sum + i
print('计算结果为:{}'.format(sum))
cal_sum()
使用装饰器,就不再需要主动调用 print_sum = time_print(cal_sum) 函数,Python 解释器会自动进行函数增强
因此回到最上面的说法,装饰器本质上是对函数闭包的语法糖,而什么是语法糖,简单概括可以是:
1)指计算机语言中添加的某种语法,方便使用
2)语法糖没有增加新功能,只是一种更方便的写法
3)语法糖可以完全等价地转换为原本非语法糖的代码
@time_print
def cal_sum():
pass
######## 等价于 ########
def cal_sum():
pass
print_sum = time_print(cal_sum)
print_sum()
装饰器在第一次调用被修饰的函数时调用闭包进行函数增强:
1)增强时机:在第一次调用之前,也就是说,只有在第一次调用被装饰的函数时,闭包函数才会被调用,若整个程序的运行过程中都没有调用被装饰的函数,闭包函数也不会被调用
2)增强次数:只增强一次,也就是说,闭包函数只被调用一次,当第二次以上调用原函数时,实际上调用的直接就是增强后的函数
三、装饰器的用法
3.1 常见装饰器
装饰器的用法有很多种,其中,最常见的是日志打印器、时间计时器
1)日志打印器
def log_wrapper(func):
def wrapper(*args, **kw):
print('start func: {}'.format(func.__name__))
func(*args, **kw)
print('end func: {}'.format(func.__name__))
return wrapper
@log_wrapper
def cal(x, y):
sum = x + y print('{} + {} = {}'.format(x, y, sum))
cal(1, 2)
2)时间计时器
def time_wrapper(func):
def wrapper(*args, **kw):
start_time = datetime.now()
func(*args, **kw)
end_time = datetime.now()
print('func: {} execution cost: {}s'.format(func.__name__, end_time - start_time))
return wrapper
@time_wrapper
def cal(x, y):
sum = x + y
print('{} + {} = {}'.format(x, y, sum))
cal(1, 2)
3.2 带参数的函数装饰器
形如 3.1 常用装饰器,不传参的装饰器只能对被修饰函数,执行固定逻辑,而装饰器本身是一个函数,既然作为函数都不能携带参数,那么这个函数的功能就非常受到限制,同时,装饰器本身是支持传参的:
def sex_check(sex):
def check_wrapper(func):
def wrapper(*args, **kwargs):
if sex == '0':
print('this is girl')
elif sex == '1':
print('this is boy')
else:
pass
func(*args, **kwargs)
return wrapper
return check_wrapper
@sex_check('1')
def say_hello():
print('hello')
say_hello()
3.3 基于类实现的装饰器
基于类实现的装饰器,必须实现 __init__ 和 __call__ 两个内置函数:
1)__init__ :接收被修饰函数
2)__call__ :实现装饰逻辑
class log:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
print('func: {} is running'.format(self.func.__name__))
return self.func(*args, **kwargs)
@log
def cal(x, y):
sum = x + y
print('{} + {} = {}'.format(x, y, sum))
cal(1, 2)
同样,基于类实现的装饰器也可以是带参数的,例如,在封装日志类的时候,往往需要定义日志的级别,这时候就需要给类装饰器传入参数去给这个函数指定日志级别了,带参数和不带参数的类装饰器区别很大:
1)__init__ :不再接收被修饰函数,而是接收传入参数
2)__call__ :接收被装饰函数,实现装饰逻辑
class log:
def __init__(self, level):
self.level = level
def __call__(self, func):
def wrapper(*args, **kwargs):
print('[{}] func: {} is running'.format(self.level, func.__name__))
func(*args, **kwargs)
return wrapper
@log('INFO')
def cal(x, y):
sum = x + y
print('{} + {} = {}'.format(x, y, sum))
cal(1, 2)
3.4 使用偏函数与类实现装饰器
大多数装饰器都是基于函数和闭包实现的,但并非实现装饰器的唯一方式,实际上,python 对某个对象是否能通过装饰器(@decorator)形式使用只有一个要求,即 decorator 必须是一个可被调用(callable)的对象,而 callable 对象,最熟悉的就是函数了,除函数以外,类也可以是 callable 对象,只要实现了 __call__ 函数,除此之外,一些使用的偏函数也是 callable 对象。
偏函数,即当函数参数太多的时候,可以选择将部分参数固定来简化函数,固定的方式就是使用 functools 模块中的 partial 函数。
import time
import functools
class DelayFunc:
def __init__(self, duration, func):
self.duration = duration
self.func = func
def __call__(self, *args, **kwargs):
print(f'Wait for {self.duration} seconds...')
time.sleep(self.duration)
return self.func(*args, **kwargs)
def eager_call(self, *args, **kwargs):
print('Call without delay')
return self.func(*args, **kwargs)
def delay(duration):
"""
装饰器:推迟某个函数的执行。
同时提供 .eager_call 方法立即执行
"""
# 此处为了避免定义额外函数,
# 直接使用 functools.partial 帮助构造 DelayFunc 实例
return functools.partial(DelayFunc, duration)
@delay(duration=2)
def add(a, b):
return a + b
print(add(1, 2))
3.5 实现能修饰类的装饰器
用 Python 写单例模式时候,常用的有三种写法,其中一种就是用装饰器来实现:
单例模式,一种常见的软件设计模式,该模式主要目的是确保某个类只有一个实例存在,当你希望整个系统中,某个类只能出现一个实例时,单例对象就能派上用场,最终的目的是为了节省开辟对象控件及销毁对象空间的时间。
instances = {}
def singleton(cls):
def get_instance(*args, **kw):
cls_name = cls.__name__
if cls_name not in instances:
instance = cls(*args, **kw)
instances[cls_name] = instance
return instances[cls_name]
return get_instance
# @singleton
class User:
_instance = None
def __init__(self, name):
self.name = name
u = User('kylin')
print(u, u.name)
u2 = User('jessea')
print(u2, u2.name)
# 执行结果
<__main__.User object at 0x0000000001D01AC0> kylin
<__main__.User object at 0x00000000025AB1C0> jessea
instances = {}
def singleton(cls):
def get_instance(*args, **kw):
cls_name = cls.__name__
if cls_name not in instances:
instance = cls(*args, **kw)
instances[cls_name] = instance
return instances[cls_name]
return get_instance
@singleton
class User:
_instance = None
def __init__(self, name):
self.name = name
u = User('kylin')
print(u, u.name)
u2 = User('jessea')
print(u2, u2.name)
# 执行结果
<__main__.User object at 0x0000000001E51EB0> kylin
<__main__.User object at 0x0000000001E51EB0> kylin
3.6 wraps 装饰器
使用 functools 模块提供的 wraps 装饰器可以避免被装饰的函数的特殊属性被更改,如函数名称 name 被更改,如果不使用该装饰器,则会导致函数名称被替换,从而导致端点(端点的默认值是函数名)出错。
不使用 wraps 时:
def test(func):
def decorated_function(*args, **kwargs):
"""decorated_function里的注释"""
print('Hello')
return func(*args, **kwargs)
return decorated_function
@decorator
def function():
"""function里的注释"""
print('World')
function()
print(function.__name__)
print(function.__doc__)
# 执行结果
Hello
World
decorated_function
decorated_function里的注释
使用 wraps 时:
from functools import wraps
def test(func):
@wraps(func)
def decorated_function(*args, **kwargs):
"""decorated_function里的注释"""
print('Hello')
return func(*args, **kwargs)
return decorated_function
@decorator
def function():
"""function里的注释"""
print('World')
function()
print(function.__name__)
print(function.__doc__)
# 执行结果
Hello
World
function
function里的注释
3.7 property 内置装饰器
@property 装饰器是 Python 内置的装饰器,主要作用是把类中的一个方法变为类中的一个属性,并且使定义属性和修改现有属性变得更容易,property 的源码解释如下:
"""
Property attribute.
fget
function to be used for getting an attribute value
fset
function to be used for setting an attribute value
fdel
function to be used for del'ing an attribute
doc
docstring
Typical use is to define a managed attribute x:
class C(object):
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.")
Decorators make defining new properties or modifying existing ones easy:
class C(object):
@property
def x(self):
"I am the 'x' property."
return self._x
@x.setter
def x(self, value):
self._x = value
@x.deleter
def x(self):
del self._x
"""
- fget:获取属性值的函数
- fset:设置属性值的函数
- fdel:删除属性值的函数
- doc:属性对象创建文档字符串
从注释文档中可以看出,装饰器使定义新属性或修改现有属性变得容易,具体如何变得容易,下面通过一个例子来进行比较,首先是传统的方法进行绑定属性和访问属性:
class Person:
def get_name(self):
return self.__name
def set_name(self, name):
self.__name = name
if __name__ == '__main__':
p = Person()
p.set_name('kylin')
print('hi,{}'.format(p.get_name()))
p.set_name(['kylin', 'jessea'])
print('hi,{}'.format(p.get_name()))
# 执行结果
hi,kylin
hi,['kylin', 'jessea']
这种方式在绑定属性,获取属性时显的很是繁琐,而且无法保证数据的准确性,从执行结果看来,名字应该是个字符串才对,然而输出结果却是个列表,并不符合实际规则,而且也没有通过直接访问属性,修改属性的方式那么直观,因此,通过 @property 对上述代码进行优化:
class Person:
@property
def name(self):
return self.__name
@name.setter
def name(self, name):
if isinstance(name, str):
self.__name = name
else:
raise TypeError('name must be str')
if __name__ == '__main__':
p = Person()
p.name = 'kylin'
print('hi,{}'.format(p.name))
p.name = ['kylin', 'jessea']
print('hi,{}'.format(p.name))
# 执行结果
hi,kylin
Traceback (most recent call last):
File "D:/workspaces/Coding/DecoratorDemo/10_decorator_phs.py", line 43, in <module>
p.name = ['kylin', 'jessea']
File "D:/workspaces/Coding/DecoratorDemo/10_decorator_phs.py", line 29, in name
raise TypeError('name must be str')
TypeError: name must be str
经过优化后的代码可以看到当绑定的属性并非是一个字符串类型时,就会报错,而且我们可以直接通过类似访问属性的方式来绑定属性,访问属性,这样就更加直观了,这里有个点需要注意,@name.setter中name这个名字及其被他修饰的方法名字与@property修改的方法名必须保持一致,否则会报错,其中@name.setter装饰器是因为使用了@property后他本身创建的装饰器。
@perproty装饰器并不仅仅只用来绑定属性和访问属性,还可以用来在类的外部访问私有成员属性,先来看个类的外部直接访问私有成员的实例:
class Person:
def __init__(self, name):
self.__name = name
if __name__ == '__main__':
p = Person('kylin')
print(p.__name)
# 执行结果
Traceback (most recent call last):
File "D:/workspaces/Coding/DecoratorDemo/10_decorator_phs.py", line 49, in <module>
print(p.__name)
AttributeError: 'Person' object has no attribute '__name'
运行时,程序报错了,原因是 Python 不允许在类的外部访问类中的私有成员,其目的是为了保护数据的安全性,这时候可以使用 @property 装饰器来访问类属性:
class Person:
def __init__(self, name):
self.__name = name
@property
def name(self):
return self.__name
if __name__ == '__main__':
p = Person('kylin')
print(p.name)
# 执行结果
kylin
相对于绑定属性来说这种方式用的比较多,当你不想子类继承父类时,防止子类修改父类的属性,那么你完全就可以使用这种方法来避免属性被修改,而且在子类和类的外部还可以正常访问这个私有属性。
总结
@property 装饰器主要用来改变一个方法为一个属性,且需要注意几点:
- 被此装饰器装饰的方法不能传递任何除 self 外的其他参数。
- 当同时使用 @property 和 @x.setter 时 需要保证 x 以及被 @x.setter 修改的方法名字与 @property 修改的方法名字必须保持一致。