上次知识回顾:Python3 与 C# 扩展之~基础衍生
终于期末考试结束了,聪明的小明同学现在当然是美滋滋的过暑假了,左手一只瓜,右手一本书~正在给老乡小张同学拓展他研究多日的知识点
装饰器这次从 C#
开始引入,上次刚讲 迭代器模式
,这次把 装饰器模式
也带一波(纯Python方向的可以选择性跳过,也可以当扩展)
其实通俗讲就是,给原有对象动态的添加一些额外的职责(毕竟动不动就改类你让其他调用的人咋办?也不符合开放封闭原则是吧~)
举个简单的例子:(https://github.com/lotapp/BaseCode/tree/master/netcore/3_Ext/Decorators)
BaseComponent.cs
/// <summary>
/// 组件的抽象父类
/// </summary>
public abstract class BaseComponent
{
/// <summary>
/// 定义一个登录的抽象方法
/// 其他方法,这边省略
/// </summary>
public abstract string Login();
}
LoginComponent.cs
/// <summary>
/// 默认登录组件(账号+密码)
/// 其他方法省略
/// 友情提醒一下,抽象类里面可以定义非抽象方法
/// </summary>
public class LoginComponent : BaseComponent
{
public override string Login()
{
return "默认账号密码登录";
}
}
默认调用:
static void Main(string[] args)
{
var obj = new LoginComponent();
var str = obj.Login();
Console.WriteLine(str);
}
如果这时候平台需要添加微信第三方登录,怎么办?一般都是用继承来解决,其实还可以通过灵活的 装饰器
来解决:(好处可以自己体会)
先定义一个通用装饰器(不一定针对登录,注册等等只要在BaseComponent中的都能用)
/// <summary>
/// 装饰器
/// </summary>
public class BaseDecorator : BaseComponent
{
protected BaseComponent _component;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="obj">登录组件对象</param>
protected BaseDecorator(BaseComponent obj)
{
this._component = obj;
}
public override string Login()
{
string str = string.Empty;
if (_component != null) str = _component.Login();
return str;
}
}
现在根据需求添加微信登录:(符合开放封闭原则)
/// <summary>
/// 默认登录组件(账号+密码)
/// 其他方法省略
/// </summary>
public class WeChatLoginDecorator : BaseDecorator
{
public WeChatLoginDecorator(BaseComponent obj) : base(obj)
{
}
/// <summary>
/// 添加微信第三方登录
/// </summary>
/// <returns></returns>
public string WeChatLogin()
{
return "add WeChatLogin";
}
}
调用:(原有系统该怎么用就怎么用,新系统可以使用装饰器来添加新功能)
static void Main(string[] args)
{
#region 登录模块V2
// 实例化登录装饰器
var loginDecorator = new WeChatLoginDecorator(new LoginComponent());
// 原有的登录方法
var str1 = loginDecorator.Login();
// 现在新增的登录方法
var str2 = loginDecorator.WeChatLogin();
Console.WriteLine($"{str1}\n{str2}");
#endregion
}
结果:
默认账号密码登录
add WeChatLogin
如果再加入QQ和新浪登录的功能就再添加一个V3版本的装饰器,继承当时V2版本的登录即可(版本迭代特别方便)
/// <summary>
/// 默认登录组件(账号+密码)
/// 其他方法省略
/// </summary>
public class LoginDecoratorV3 : WeChatLoginDecorator
{
public LoginDecoratorV3(BaseComponent obj) : base(obj)
{
}
/// <summary>
/// 添加QQ登录
/// </summary>
/// <returns></returns>
public string QQLogin()
{
return "add QQLogin";
}
/// <summary>
/// 添加新浪登录
/// </summary>
/// <returns></returns>
public string SinaLogin()
{
return "add SinaLogin";
}
}
调用:
static void Main(string[] args)
{
#region 登录模块V3
// 实例化登录装饰器
var loginDecoratorV3 = new LoginDecoratorV3(new LoginComponent());
// 原有的登录方法
var v1 = loginDecoratorV3.Login();
// 第二个版本迭代中的微信登录
var v2 = loginDecoratorV3.WeChatLogin();
// 新增的QQ和新浪登录
var qqLogin = loginDecoratorV3.QQLogin();
var sinaLogin = loginDecoratorV3.SinaLogin();
Console.WriteLine($"{v1}\n{v2}\n{qqLogin}\n{sinaLogin}");
#endregion
}
结果:
默认账号密码登录
add WeChatLogin
add QQLogin
add SinaLogin
其实还有很多用处,比如原有系统缓存这块当时考虑不到,现在并发来了,已经上线了,原有代码又不太敢大幅度修改,这时候装饰器就很方便的给某些功能添加点缓存、测试、日记等等系列功能
实际场景说的已经很明白了,其他的自己摸索一下吧
那Python怎么实现装饰器呢?小胖问道。
小明屁颠屁颠的跑过去说道,通过闭包咯~(闭包如果忘了,可以回顾一下)
来看一个应用场景,以前老版本系统因为并发比较小,没考虑到缓存
def get_data():
print("直接数据库读取数据")
def main():
get_data()
if __name__ == '__main__':
main()
在不修改原有代码的前提下咋办?我们参照C#和Java写下如下代码:
# 添加一个闭包
def cache(func):
def decorator():
print("给功能添加了缓存")
if True:
pass
else:
func()# 如果缓存失效则读取数据库获取新的数据
return decorator
def get_data():
print("直接数据库读取数据")
def main():
f1 = cache(get_data)
f1()
print(type(f1))
if __name__ == '__main__':
main()
给功能添加了缓存
<class 'function'>
小张问道:“怎么也这么麻烦啊,C#的那个我就有点晕了,怎么Python也这样啊?” f1=cache(get_data)
f1()
小明哈哈一笑道:“人生苦短,我用Python~这句话可不是随便说着玩的,来来来,看看Python的语法糖”:
def cache(func):
def wrapper():
print("给功能添加了缓存")
if True:
pass
else:
func() # 如果缓存失效则读取数据库获取新的数据
return wrapper
@cache
def get_data():
print("直接数据库读取数据")
def main():
get_data()
if __name__ == '__main__':
main()
给功能添加了缓存
其实
@cache
def get_data()
等价于
# 把f1改成函数名字罢了。可以这么理解:get_data重写指向了一个新函数
get_data = cache(get_data)
小张同学瞪了瞪眼睛,努力回想着以前的知识点,然后脱口而出:“这不是我们之前讲的属性装饰器吗?而且好方便啊,这完全符合开放封闭原则啊!“
class Student(object):
def __init__(self, name, age):
# 一般需要用到的属性都直接放在__init__里面了
self.name = name
self.age = age
@property
def name(self):
return self.__name
@name.setter
def name(self, name):
self.__name = name
@property
def age(self):
return self.__age
@age.setter
def age(self, age):
if age > 0:
self.__age = age
else:
print("age must > 0")
def show(self):
print("name:%s,age:%s" % (self.name, self.age))
小明也愣了愣,说道:”也对哦,你不说我都忘了,我们学习面向对象三大特性的时候经常用呢,怪不得这么熟悉呢“
随后又嘀咕了一句:”我怎么不知道开放封闭原则...“
小张嘲笑道:”这你都不知道?对扩展开放,对已经实现的代码封闭嘛~“
# 需要注意一点
def cache(func):
print("装饰器开始装饰")
def wrapper():
print("给功能添加了缓存")
if True:
pass
else:
func() # 如果缓存失效则读取数据库获取新的数据
return wrapper
@cache # 当你写这个的时候,装饰器就开始装饰了,闭包里面的功能是你调用的时候执行
def get_data():
print("直接数据库读取数据")
装饰器开始装饰
小明赶紧扯开话题,”咳咳,我们接下来我们接着讲装饰器"
小张问道,像上面那个第三方登录的案例,想加多少加多少,Python怎么办呢?
小明一笑而过~
现在项目又升级了,要求每次调用都要打印一下日记信息,方便以后纠错,小张先用自己的理解打下了这段代码,然后像小明请教:
def log(func):
def wrapper():
print("输出日记信息")
cache(func)()
return wrapper
def cache(func):
def wrapper():
print("给功能添加了缓存")
if True:
pass
else:
func() # 如果缓存失效则读取数据库获取新的数据
return wrapper
@log
def get_data():
print("直接数据库读取数据")
def main():
get_data()
if __name__ == '__main__':
main()
输出日记信息
给功能添加了缓存
小明刚美滋滋的喝着口口可乐呢,看到代码后一不小心喷了小张一脸,然后尴尬的说道:“Python又不是只能装饰一个装饰器,来看看我的代码”:
def log(func):
print("开始装饰Log模块")
def wrapper():
print("输出日记信息")
func()
return wrapper
def cache(func):
print("开始装饰Cache模块")
def wrapper():
print("给功能添加了缓存")
if True:
pass
else:
func() # 如果缓存失效则读取数据库获取新的数据
return wrapper
@log
@cache
def get_data():
print("直接数据库读取数据")
def main():
get_data()
if __name__ == '__main__':
main()
开始装饰Cache模块
开始装饰Log模块
输出日记信息
给功能添加了缓存
小张耐心的看完了代码,然后说道:“咦,我发现它装饰的时候是从下往上装饰,执行的时候是从上往下啊?执行的时候程序本来就是从上往下,按照道理应该是从上往下装饰啊?”
小明神秘的说道:“你猜啊~你可以把它理解为寄快递和拆快递”
小张兴奋的跳起来了:
装饰器:装快递,先包装里面的物品,然后再加个盒子。执行装饰器:拆快递,先拆外面的包装再拆里面的~简直妙不可言啊
小明继续讲述他哥哥的血泪历史:
需求时刻在变,系统使用范围更广了,为了不砸场子,抠门的老板决定每年多花5W在技术研发的硬件支持上,这下子技术部老开心了,想想以前前端只能通过CDN和HTTP请求来缓存,后端只能依赖页面缓存和数据库缓存就心塞,于是赶紧新增加一台Redis的云服务器。为了以后和现在缓存代码得变一变了,需要支持指定的缓存数据库:(如果不是维护别人搞的老项目,你这么玩保证被打死,开发的时候老老实实的工厂模式搞起)
带参数的装饰器一般都是用来记录logo日记比较多,自己开发知道debug模式,生产指定except模式等等
# 可以理解为,在原来的外面套了一层
def cache(cache_name):
def decorator(func):
def wrapper():
if cache_name == "redis":
print("给功能添加了Redis缓存")
elif cache_name == "memcache":
pass
else:
func()
return wrapper
return decorator
@cache("redis") # 相当于是:get_data = cache(”redis“)(get_data)
def get_data():
print("直接数据库读取数据")
def main():
get_data()
if __name__ == '__main__':
main()
给功能添加了Redis缓存
小张很高兴,然后练了练手,然后质问小明道:”你是不是藏了一手!“
代码如下:
def log(func):
def inner():
print("%s log_info..." % func.__name__)
func()
return inner
@log
def login_in(name_str, pass_str):
return "欢迎登录:%s" % (name_str)
@log
def login_out():
print("已经退出登录")
@log
def get_data(id):
print("%s:data xxx" % id)
def main():
login_out()
get_data(1)
print(login_in("小明", "xxx"))
if __name__ == '__main__':
main()
login_out log_info...
已经退出登录
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-7-dcb695819107> in <module>()
23
24 if __name__ == '__main__':
---> 25 main()
<ipython-input-7-dcb695819107> in main()
19 def main():
20 login_out()
---> 21 get_data(1)
22 print(login_in("小明", "xxx"))
23
TypeError: inner() takes 0 positional arguments but 1 was given
小明尴尬的笑了下,然后赶紧倾囊相授,定义一个通用的装饰器:(传参数就在外面套一层)
def log(func):
@functools.wraps(func) # 签名下面一个案例就会讲
def wrapper(*args,**kv):
"""可变参 + 关键字参数"""
print("%s log_info..." % func.__name__)
return func(*args,**kv)
return wrapper
这部分知识如果忘记了可以回顾一下,我们之前讲的函数系列:https://www.cnblogs.com/dotnetcrazy/p/9175950.html
def log(func):
# 可变参 + 关键字参数
def wrapper(*args,**kv):
print("%s log_info..." % func.__name__)
return func(*args,**kv)
return wrapper
@log
def login_in(name_str, pass_str):
return "欢迎登录:%s" % (name_str)
@log
def login_out():
print("已经退出登录")
@log
def get_data(id):
print("%s:data xxx" % id)
def main():
login_out()
get_data(1)
print(login_in("小明", "xxx"))
if __name__ == '__main__':
main()
login_out log_info...
已经退出登录
get_data log_info...
1:data xxx
login_in log_info...
欢迎登录:小明
其实装饰器可以做很多事情,比如强制类型检测等,先看几个扩展:
成也装饰器,败也装饰器,来个案例看看,装饰器装饰的函数真的就对原函数没点影响?
# 添加一个闭包
def cache(func):
def wrapper(*args,**kv):
if True:
print("缓存尚未失效:直接返回缓存数据")
else:
func(*args,**kv)
return wrapper
def get_data(id):
"""获取数据"""
print("通过%d直接数据库读取数据"%id)
# 进行装饰
get_data = cache(get_data)
# 调用原有名称的函数
get_data(110)
# 发现虽然函数调用时候的名字没有变
# 但是内部签名却变成了闭包里面的函数名了
print(get_data.__name__)
print(get_data.__doc__)
# print(get_data.__annotations__)
缓存尚未失效:直接返回缓存数据
wrapper
None
发现虽然函数调用时候的名字没有变,但是内部签名却变成了闭包里面的函数名了!
玩过逆向的人都知道,像你修改了apk文件,它看似一样,但签名就变了,得再处理才可能绕过原来的一些自效验的验证措施
这边一样的道理,你写了一个装饰器作用在某个函数上,但是这个函数的重要的元信息比如名字、文档字符串、注解和参数签名都丢失了。
functools
里面的 wraps
就帮我们干了这个事情(之前讲模块的时候引入了functools,随后讲衍生的时候用了里面的偏函数,这边讲讲 wraps
)
上面代码改改:
from functools import wraps
# 添加一个闭包
def cache(func):
@wraps(func)
def wrapper(*args,**kv):
if True:
print("缓存尚未失效:直接返回缓存数据")
else:
func(*args,**kv)
return wrapper
def get_data(id):
"""获取数据"""
print("通过%d直接数据库读取数据"%id)
# 进行装饰
get_data = cache(get_data)
# 调用原有名称的函数
get_data(110)
# 签名已然一致
print(get_data.__name__)
print(get_data.__doc__)
# print(get_data.__annotations__)
缓存尚未失效:直接返回缓存数据
get_data
获取数据
另外: @wraps
有一个重要特征是它能让你通过属性 __wrapped__
直接访问被包装函数,eg:
get_data.__wrapped__(100)
通过100直接数据库读取数据
import logging
from functools import wraps, partial
def logged(func=None, *, level=logging.DEBUG, name=None, message=None):
if func is None:
return partial(logged, level=level, name=name, message=message)
logname = name if name else func.__module__
log = logging.getLogger(logname)
logmsg = message if message else func.__name__
@wraps(func)
def wrapper(*args, **kwargs):
log.log(level, logmsg)
return func(*args, **kwargs)
return wrapper
@logged
def add(x, y):
return x + y
@logged(level=logging.CRITICAL, name='测试')
def get_data():
print("读数据ing")
def main():
add(1,2)
get_data()
if __name__ == '__main__':
main()
get_data
读数据ing
在类里面定义装饰器很简单,但是你首先要确认它的使用方式。比如到底是作为一个实例方法还是类方法:(别忘记写 self
和 cls
)
from functools import wraps
class A(object):
# 实例方法
def decorator1(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
print("实例方法装饰器")
return func(*args, **kwargs)
return wrapper
# 类方法
@classmethod
def decorator2(cls, func):
@wraps(func)
def wrapper(*args, **kwargs):
print("类方法装饰器")
return func(*args, **kwargs)
return wrapper
# 装饰方式不一样
a = A()
@a.decorator1 # 实例方法调用
def test1():
pass
@A.decorator2 # 类方法调用
def test2():
pass
# 调用一下
test1()
test2()
实例方法装饰器
类方法装饰器
在涉及到继承的时候。 例如,假设你想让在A中定义的装饰器作用在子类B中。你需要像下面这样写:
class B(A):
@A.decorator2
def test(self):
pass
也就是说,装饰器要被定义成类方法并且你必须显式的使用父类名去调用它。
你不能使用 @B.decorator2
,因为在方法定义时,这个类B还没有被创建。
看这个之前,我们先来看看怎么把类当函数一样使用:
class A(object):
def __call__(self):
print("让类对象能像函数一样调用的~魔法方法")
def main():
a = A()
a()
if __name__ == '__main__':
main()
让类对象能像函数一样调用的~魔法方法
重载这些魔法方法一般会改变对象的内部行为。上面这个例子就让一个类对象拥有了被调用的行为。
装饰器函数其实是这样一个接口约束,它必须接受一个 callable
对象作为参数,然后返回一个 callable
对象。
在Python中一般 callable
对象都是函数,但也有例外。只要某个对象重写了 __call__()
方法,那么这个对象就是callable的
用类来实现呢?我们可以让类的构造函数 __init__()
接受一个函数,然后重载 __call__()
并返回一个函数,也可以达到装饰器函数的效果
我们拿之前说的通用装饰器的例子继续说:(一般来说装饰器就定义成方法,然后给需要添加的函数或者类方法添加就基本够用了)
from functools import wraps
class Log(object):
def __init__(self, func):
wraps(func)(self) # @wraps(func) 访问不到,所以用这种方式
self.__func = func
def __call__(self, *args, **kvs):
print("%s log_info..." % self.__func.__name__)
return self.__func(*args, **kvs)
@Log
def login_in(name_str, pass_str):
return "欢迎登录:%s" % (name_str)
@Log
def login_out():
print("已经退出登录")
@Log
def get_data(id):
print("%s:data xxx" % id)
def main():
login_out()
get_data(1)
print(login_in("小明", "xxx"))
if __name__ == '__main__':
main()
login_out log_info...
已经退出登录
get_data log_info...
1:data xxx
login_in log_info...
欢迎登录:小明
对类进行装饰的测试:(以上一个案例为例)
装饰实例方法的时候容易出现莫名其妙的错误,所以一般加上get方法(反射系列的稍后会讲)
eg:show()missing1required positional argument:'self'
完整写法:(你可以去除 __get__
试试)
import types
from functools import wraps
class Log(object):
def __init__(self, func):
wraps(func)(self) # @wraps(func) 访问不到,所以用这种方式
self.__func = func
def __call__(self, *args, **kvs):
print("%s log_info..." % self.__func.__name__)
return self.__func(*args, **kvs)
# 装饰实例方法的时候容易出现莫名其妙的错误,所以一般加上get方法
# eg:show() missing 1 required positional argument: 'self'
def __get__(self, instance, cls):
if instance is None:
return self
else:
return types.MethodType(self, instance)
class LoginComponent(object):
def __init__(self, name):
self.__name = name
@Log
def show(self):
"""实例方法"""
print("欢迎你:%s" % self.__name)
@classmethod
@Log # 写在下面("从下往上装,从上往下拆")
def login_in(cls):
"""类方法"""
print("登录ing")
@staticmethod
@Log
def show_news():
"""静态方法"""
print("今天的新闻是...")
def main():
LoginComponent.login_in()
LoginComponent.show_news()
login = LoginComponent("小明")
login.show()
if __name__ == '__main__':
main()
login_in log_info...
登录ing
show_news log_info...
今天的新闻是...
show log_info...
欢迎你:小明
更多的可以参考如下链接:
详解Python装饰器
将装饰器定义为类
Python中的init()和call()函数
python中装饰器的使用和类装饰器在类中方法的使用