社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

怎么在Python装饰器中自定义功能呢?用这种方法让你“为所欲为”

承志 • 5 年前 • 573 次点击  
阅读 15

怎么在Python装饰器中自定义功能呢?用这种方法让你“为所欲为”

本文始发于个人公众号: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,同样可以在许多其他的领域获得突飞猛进的进步。

各位看官大人,赏个关注吧~

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/62855
 
573 次点击