Python3 与 C# 扩展之~装饰器专栏

上次知识回顾:Python3 与 C# 扩展之~基础衍生

终于期末考试结束了,聪明的小明同学现在当然是美滋滋的过暑假了,左手一只瓜,右手一本书~正在给老乡小张同学拓展他研究多日的知识点

1.NetCore装饰器模式

装饰器这次从 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

其实还有很多用处,比如原有系统缓存这块当时考虑不到,现在并发来了,已经上线了,原有代码又不太敢大幅度修改,这时候装饰器就很方便的给某些功能添加点缓存、测试、日记等等系列功能

实际场景说的已经很明白了,其他的自己摸索一下吧

2.Python装饰器

那Python怎么实现装饰器呢?小胖问道。

小明屁颠屁颠的跑过去说道,通过闭包咯~(闭包如果忘了,可以回顾一下)

2.1.装饰器引入

来看一个应用场景,以前老版本系统因为并发比较小,没考虑到缓存

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("直接数据库读取数据")
装饰器开始装饰

2.2.多个装饰器

小明赶紧扯开话题,”咳咳,我们接下来我们接着讲装饰器"

小张问道,像上面那个第三方登录的案例,想加多少加多少,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模块
输出日记信息
给功能添加了缓存

小张耐心的看完了代码,然后说道:“咦,我发现它装饰的时候是从下往上装饰,执行的时候是从上往下啊?执行的时候程序本来就是从上往下,按照道理应该是从上往下装饰啊?”

小明神秘的说道:“你猜啊~你可以把它理解为寄快递和拆快递

小张兴奋的跳起来了:

装饰器:装快递,先包装里面的物品,然后再加个盒子。执行装饰器:拆快递,先拆外面的包装再拆里面的~简直妙不可言啊

2.3.带参装饰器

小明继续讲述他哥哥的血泪历史:

需求时刻在变,系统使用范围更广了,为了不砸场子,抠门的老板决定每年多花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

2.4.通用装饰器

小明尴尬的笑了下,然后赶紧倾囊相授,定义一个通用的装饰器:(传参数就在外面套一层)

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...
欢迎登录:小明

2.5.扩展补充

其实装饰器可以做很多事情,比如强制类型检测等,先看几个扩展:

1.装饰器方法签名的问题

成也装饰器,败也装饰器,来个案例看看,装饰器装饰的函数真的就对原函数没点影响?

# 添加一个闭包
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直接数据库读取数据

2.装饰器传参的扩展(可传可不传)

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

3.类中定义装饰器

在类里面定义装饰器很简单,但是你首先要确认它的使用方式。比如到底是作为一个实例方法还是类方法:(别忘记写 selfcls

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还没有被创建。

4.类装饰器

看这个之前,我们先来看看怎么把类当函数一样使用:

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中装饰器的使用和类装饰器在类中方法的使用

原文发布于微信公众号 - 我为Net狂(dotNetCrazy)

原文发表时间:2018-07-17

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏蜉蝣禅修之道

EJBCA使用之注册用户及创建证书

29940
来自专栏农夫安全

代码审计之命令执行漏洞

环境:windows + apache + mysql + php (phpstudy) 由于是在Windows下进行的测试,所以和Linux下的测试会有所不...

29060
来自专栏liuchengxu

[译]27个Jupyter Notebook小提示与技巧

Jupyter notebook, 前身是 IPython notebook, 它是一个非常灵活的工具,有助于帮助你构建很多可读的分析,你可以在里面同时保留代码...

30320
来自专栏自由而无用的灵魂的碎碎念

Tips in Visual Studio 2008

.NET几乎程序员都在使用visual studio 2008进行开发。可是,你通过它达到最大的开发效率了吗?

12720
来自专栏Java后端技术

基于Spring的可扩展Schema进行开发自定义配置标签支持

  最近和朋友一起想开发一个类似alibaba dubbo的功能的工具,其中就用到了基于Spring的可扩展Schema进行开发自定义配置标签支持,通过上网查资...

8830
来自专栏owent

C++又一坑:动态链接库中的全局变量

前几天我们项目的日志系统出现了一点问题,但是一直没有时间去深究。 昨天在同事的帮助下,无意中猜了一种可能性,结果还真被我猜中了,于是今天就特别研究了一下,记录...

25830
来自专栏Golang语言社区

GO语言高并发学习心得体会例

<div class="blockcode"><blockquote>信号 sigRecv1:=make(chan os.Signal,1) sigs1...

35980
来自专栏Golang语言社区

多线程编程10个例子--2

// TODO: Add extra initialization here m_ctrlProgress.SetRange(0,99); m_nMilliSe...

84870
来自专栏扎心了老铁

python使用装饰器@函数式化django开发

django是一个python web开发的框架。作为一个框架MVC的架构已经实现起来了。但是编码的时候你经常要进行进一步的抽象。 AOP是一种称为面向切面的开...

55970
来自专栏大数据文摘

Jupyter Notebook的27个窍门,技巧和快捷键

686110

扫码关注云+社区

领取腾讯云代金券