专栏首页TechFlow还有这种操作,可以让装饰器为所欲为?

还有这种操作,可以让装饰器为所欲为?

今天是Python专题的第14篇文章,我们继续装饰器的话题,来看看怎么给装饰器包装方法,实现更多灵活的操作。

在之前的文章当中,我们实现了对装饰器赋予参数,从而可以通过传入不同的参数来控制装饰器中的逻辑。这样做可以大大地增加装饰器的灵活性,但是仍然不足以解决所有的问题。

如果我们面临一个变动很频繁的业务,以后也许需要加上一些当前想不到的逻辑,这个时候就没有办法仅仅通过参数来控制了。那么有没有办法不仅仅是传入参数,而是可以给装饰器添加不同的逻辑呢?

当然是有的,但是这个操作比较复杂,让我们抽丝剥茧,一点一点来吃透它。

setattr和getattr操作

首先我们来看下setattr和getattr这两个方法,attr是attribute的缩写,也就是属性的意思。我们搞明白了这个单词的意思之后就简单了,根据字面可以理解到,这两个方法一个是设置属性一个是获取属性

是的,就是这么简单,没错。

其中getattr尤其简单,基本上等价于使用.去获取属性。

我们来看一个最简单的例子,我们先创建一个类,然后给它附上一个属性。

class A:
    def __init__(self):
        self.name = 'hello'

之后,我们可以使用getattr方法去获得它的name属性:

a = A()
getattr(a, 'name')

有get自然就有set,我们也可以通过setattr为它附上新的属性。第二个参数是新增的属性名称,第三个参数是属性的值。

setattr(a, 'age', 18)

这样,当我们去执行a.age的时候,就会获得18。这里要注意的是,我们只是单纯地为a这个实例创建了新的属性,并没有更改A这个类中的定义。所以其他A这个类的实例并不会受到影响,另外如果我们将多个值赋值给了同一个属性名会发生覆盖,也就是后面的覆盖前面的。

属性这个词在Python中的定义是比较宽泛的,除了变量可以称作是属性,函数也一样可以作为属性。也就是说我们除了可以添加一个变量之外,也可以添加一个函数。

我们来看个例子:

def print_log():
    print('This is a log')

这是一个简单的demo方法,我们通过setattr将它赋值给实例a,那么我们就可以在实例a中调用它了。

不仅仅如此,类也一样可以通过setattr方法设置。

理解了setattr和getattr的用法之后,我们不禁有一个问题,我们通过.操作不香吗,为什么还要搞一个setattr和getattr出来呢?

如果我们自己写代码写着玩,当然是用.操作更方便,但如果是实际的开发场景。很有可能我们需要添加的属性的名称是从别的地方获取的,而不是写死的,也就是说是可配置的。这个时候就不能通过.了,我们考虑问题的时候不能仅仅从功能入手,也需要思考一下它的使用场景。

为装饰器定义属性

setattr我们都已经熟悉了,接下来回到正题。Python当中一切都是对象,同样函数也是对象。既然函数也是对象,那么我们就可以给函数也设置属性。装饰器的本质就是函数,所以我们可以给装饰器内包装的函数也设置属性,为了方便大家理解,我先不用setattr,让大家看看单纯的带属性的装饰器是什么样的。

def decorate(func):
    logmsg = func.__name__

    @wraps(func)
    def wrapper(*args, **kwargs):
        print(logmsg)
        return func(*args, **kwargs)

    def set_message(newmsg):
        nonlocal logmsg
        logmsg = newmsg
        
    wrapper.set_message = set_message
    return wrapper

如果我们把set_message这个方法拿掉的话,它就是一个普普通通的装饰器。set_message方法当中,我们使用nonlocal关键字修改了logmsg这个变量的值,而这个值会在装饰器的包装函数当中用到。也就是说我们通过调用set_message方法,可以修改这个装饰器的运行结果和逻辑。

这里,我们没用装饰器,而是简单地使用了.关键字来对它进行了赋值。还是和之前说的一样,这样当然是可以的,但是如果我们想要配置这个name就做不到了。最常见的场景就是区分线上和测试环境,一种做法是在接口的名字之前加上一个标识,比如线上是online,测试环境是test或者是dev。通过这种方法区分不同环境的逻辑。

所以比较好的方法是将这个逻辑也写成一个装饰器,将被包装的方法作为参数传入。如果你看明白了上一篇文章,熟悉装饰器传参的话,这段代码对你来说应该很简单。

def attach(obj):
    @wraps(obj)
    def wrapper(func):
        setattr(obj, func.__name__, func)
        return func
    return wrapper

有了attach这个装饰器之后,我们只需要给set_message这个方法加上注解,将被包装的函数作为参数传入即可。

@attach(wrapper)
def set_message(newmsg):
    nonlocal logmsg
    logmsg = newmsg

如果只是想要实现功能,而不追求规范的话,可以使用partial来简化代码,减少它的层次结构:

def attach(obj, func=None):
    if func is None:
        return partial(attach_wrapper, obj)
    setattr(obj, func.__name__, func)
    return func

这样写也是可以work的,只要熟悉partial的用法,应该也不难理解。

让函数为所欲为

如果你是一个程序员,你面临一个变动很频繁的业务,你无法预知之后的需求情况,想要代码有足够大的机动余地,这个时候可以利用强大的setattr给程序留一个“后门”,方便后面临时修改。

具体的做法其实很简单,我们在装饰器当中定义一个dict,用来存储自定义的函数。再实现一个set_func方法将自定义的函数存储进这个dict当中,只有就可以通过参数,在不修改装饰器的情况下自由变更装饰器内的逻辑了。

我们来看代码:

def decorate(func):
    func_dict = {}

    @wraps(func)
    def wrapper(*args, **kwargs):
        # 通过key来选择应该调用哪一个函数作为装饰器的逻辑
        if kwargs.get('key') is not None:
            func_dict[kwargs['key']](*args, **kwargs)
        return func(*args, **kwargs)
        
    # 将函数名和函数作为参数传入,存储在dict中
    @attach(wrapper)
    def set_func(func_name, func):
        nonlocal func_dict
        func_dict[func_name] = func

    return wrapper

我们再来看一个使用的例子:

def test(*args, **kw):
    print('test')
add.set_func('test', test)
add(3, 4, key='test')

这样,我们就把test方法中的逻辑放入了装饰器当中,只要我们需要,我们还可以写出其他的方法,来自定义我们对装饰器的需求,而又不需要修改装饰器内部的逻辑。不仅如此,我们还可以在主体函数的前后都加上这样的逻辑,真的可以说是为所欲为了。

当然一般情况下我们用不到这样的骚操作,但是能够写出来或者说看懂这样的功能,那就说明关于装饰器的理解已经算是入门了。

结尾

装饰器可以说是函数式编程在Python当中最重要的使用渠道,在许多Python工具和框架当中大量使用。其实我们学习的并不仅仅是装饰器的一两种奇淫技巧,也是函数式编程的一些思想和理念。当我们将这些理念理解深刻了之后,不仅仅是Python,同样可以在许多其他的领域获得突飞猛进的进步。

本文分享自微信公众号 - TechFlow(techflow2019),作者:梁唐

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-05-11

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

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Python装饰器不会传参?别着急,这篇文章为你解惑

    今天是Python专题的第13篇文章,上一篇文章当中我们介绍了Python装饰器的定义和基本的用法,这篇文章我们一起来学习一下Python装饰器的一些进阶使用方...

    TechFlow-承志
  • 数据处理 | pandas入门专题——离散化与one-hot

    在上一篇文章当中我们介绍了对dataframe进行排序以及计算排名的一些方法,在今天的文章当中我们来了解一下dataframe两个非常重要的功能——离散化和on...

    TechFlow-承志
  • Numpy专题最后一篇,随机数、线性代数与持久化

    在我们做机器学习模型的研究或者是学习的时候,在完成了训练之后,有时候会希望能够将相应的参数保存下来。否则的话,如果是在Notebook当中,当Notebook关...

    TechFlow-承志
  • 了解Python装饰器

    一 装饰器是什么 装饰器是一个用于封装函数或者类的代码工具,显式地将封装器作用于函数或者类上,达到程序运行时动态增加功能的目的。对于函数运行前处理常见前置条件...

    用户1278550
  • 2-VII-RecyclerView事件

    张风捷特烈
  • 005.Kickstart部署多系统

    TFTP:共享pxelinux.0、initrd.img、vmlinux、isolinux.cfg、boot.msg、vesamenu.c32、splash.p...

    木二
  • 从实现装饰者模式中思考C++指针和引用的选择

    从实现装饰者模式中思考C++指针和引用的选择 最近在看设计模式的内容,偶然间手痒就写了一个“装饰者”模式的一个实例。该实例来源于风雪涟漪的博客,我对它做了简化。...

    Florian
  • 我被 Kotlin 和 Android 两个官方约谈了

    废话不多说,这次我邀请到了 JetBrains 的范圣佑和 Google Android 团队的 Fred 来到我的视频里,一起聊了聊 Kotlin 的未来——...

    扔物线
  • R 集成算法② bagging

    用户1359560
  • 腾讯云视频云(点播&直播)产品月刊(2020.2)

    3、推出9.9元体验包,包含流量,存储,普通转码,极速高清转码,视频审核时长五种资源类型,低成本快速体验点播所有功能。

    腾讯云视频

扫码关注云+社区

领取腾讯云代金券