谈一谈装饰器
By 青衣极客 Blue Geek In 2019-10-23
在python程序中常常看到某些函数前有一个@符号语句修饰,这就是装饰器,也称修饰器。无论是在一些第三方的库中还是python内置的模块,装饰器都有大量使用的案例。使用装饰器可以改变所定义函数的一些行为,或者在所定义函数运行之前进行一些参数检查的工作,这个语法在实际使用中为开发者带来极大的便利。特别是对于某一些函数可以使用同样的装饰器进行修饰的时候,代码重用程度得到很不错地提升。
1. 常用的装饰器
python内建的装饰器也有不少,这里就讨论一下最常用的几个。在定义一个类时,有时需要封装一些函数,这些函数不依赖具体的实例对象,而是属于类本身。这时就需要用到staticmethod或者classmethod这两个修饰器。这样可以比较紧凑且合乎逻辑地组织代码。此外还有一个常用的修饰器是property,这个修饰器的作用是将一个函数名包装成对象可读属性。与之配套的还可以这是这个可读属性的可写属性。以下演示就足以说明这几个常用属性的使用方法。
class Person:
def __init__(self, name='', age=0):
self.name = name
self.age = age
@property
def person_age(self):
"""
功能:用于读取年龄属性,对外属性名:person_age
"""
return self.age
@person_age.setter
def person_age(self, value):
"""
功能:用于设置年龄属性,对外属性名:person_age
"""
if value < 0:
raise ValueError('不允许年龄为负值')
self.age = value
@property
def person_name(self):
"""
功能:用于读取姓名字属性,对外属性名:person_name,只读
"""
return self.name
@staticmethod
def check(person):
"""
功能:静态方法,检查年龄
"""
return person.person_age < 18
@classmethod
def verify(cls, person):
"""
功能:类方法,验证年龄
"""
return person.person_age > 18
p = Person('hello', 13)
print('name={}, age={}'.format(p.person_name, p.person_age))
# 重新设置年龄
p.person_age = 1
print('重新设置person_age属性, name={}, age={}'.format(p.person_name, p.person_age))
print('Person.check =', Person.check(p))
print('Person.verify =', Person.verify(p))
# 测试设置只读属性
try:
p.person_name = '123'
except Exception as e:
print('设置person_name, 出现异常: {}'.format(e))
#测试设置不合规年龄
try:
p.person_age = -1
except Exception as e:
print('设置person_age, 出现异常: {}'.format(e))
name=hello, age=13
重新设置person_age属性, name=hello, age=1
Person.check = True
Person.verify = False
设置person_name, 出现异常: can't set attribute
设置person_age, 出现异常: 不允许年龄为负值
从以上的几个修饰器使用例子可以看出,修饰器的作用主要是在函数运行之前进行一些预处理工作,特别是参数检查和验证,函数执行环境准备以及函数调用方式调整。proprtty从修饰器可以提供只读属性,这对于类的封装而言可以控制得更加精细一点。
2. 装饰器原理和深坑
装饰器用起来显得非常神奇和神秘,那么装饰器的原理是怎么样的呢?说到底,装饰器就是使用一个函数包装另一个函数。说得更加通用一点,装饰器就是使用一个可调用对象包装另一个可调用对象。函数是一种可调用对象,定义了__call__方法的类所创建的对象也是一个可调用对象,所以函数和带有__call__方法的对象都可以作为修饰器或者被修饰器修饰。那么有个问题,函数参数该如何传递呢?在python中,函数参数有两大类,一类是位置参数,一类是键值对参数。这两个参数正好可以使用元组传参和词典传参解决,关于python中参数传递的问题可以阅读我的另一篇文章https://www.toutiao.com/i6737184785585668619/ 。说起来可能比较绕,以下使用函数这种可调用对象演示一下修饰器的原理。
# 有缺陷的装饰器实现
def hello_dec(func):
def wrapper(*args, **kwargs):
print('进入装饰器')
return func(*args, **kwargs)
return wrapper
@hello_dec
def func(pos, state=True):
print('pos={}, state={}'.format(pos, state))
func(1, False)
print(func.__name__)
进入装饰器
pos=1, state=False
wrapper
从以上演示中可以看出,我们在调用所定义的函数之前,修饰器的函数先被调用,然后修饰器的函数在调用我们所定义的函数。普通使用这样的修饰器也没什么大问题,但是我们查看以下所定义函数的一些属性,发现不对劲,因为函数属性被修改成修饰器中函数的属性了,这一点并不是我们所期望的。要就解决这个问题,就需要使用python中的一个模块来对修饰器中函数接口进行一次装饰。
# 改善的装饰器实现
from functools import wraps
def hello_dec(func):
# 深坑测试:装饰器外调用顺序
print('hello_dec开始')
@wraps(func) # 解决函数属性不一致问题
def wrapper(*args, **kwargs):
# 深坑测试:装饰器外调用顺序
print('进入装饰器')
return func(*args, **kwargs)
# 深坑测试:装饰器外调用顺序
print('hello_dec结束')
return wrapper
@hello_dec
def func(pos, state=True):
print('pos={}, state={}'.format(pos, state))
func(1, False)
print(func.__name__)
hello_dec开始
hello_dec结束
进入装饰器
pos=1, state=False
func
使用@wraps()装饰一些修饰器内部的函数之后,外部所定义函数的属性就符合预期了。我们在修饰器的一些测试点打印了信息,标记结果显示,函数中语句的执行顺序可能和大家预想得不一致,但却是符合程序运行逻辑的。因此,不建议在修饰器的函数外部进行任何操作,因为这些操作的具体执行情况可能与开发者预想的不一致。也就是说,如果开发者不明确知道修饰器中程序执行的逻辑,尽量将所有的逻辑都写入到修饰器内部的函数中,从而保证正常执行。
3. 带参数的装饰器
在使用第三方库的时候,有时会向装饰器传入参数,比如django中的admin.register()注册修饰器。其实这种带参数的修饰器只是将原有的修饰器多加了一层函数封装而已。以下的演示可以说明带参数的装饰器的定义方法。
from functools import wraps
# 定义带参数的装饰器
def print_text(text):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print('传入修饰器的参数:{}'.format(text))
return func(*args, **kwargs)
return wrapper
return decorator
@print_text('before')
def func(pos, state=True):
print('pos={}, state={}'.format(pos, state))
func(1, state=False)
print(func.__name__)
传入修饰器的参数:before
pos=1, state=False
func
到此,关于python中的装饰器讨论完毕。这里是指谈论了装饰器的基本原理和几个最常用的装饰器,在实际使用中,装饰器可以做的事情非常多,如果打算更深入了解python的话,装饰器是必须要掌握的,因为开发工程量稍微大一点就需要考虑利用装饰器的特性提升开发效率。

COMMENT
博客评论区功能由Github Issue提供,提交Issue时请以本文标题为话题。
"BG37-谈一谈装饰器"