python装饰器详解

0 前言

Python装饰器看起来类似Java中的注解,然鹅和注解并不相同,不过同样能够实现面向切面编程。
装饰器本质上是一个 Python函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象。它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能。
不过,想要理解Python中的装饰器,不得不先理解闭包(closure)这一概念。

1 闭包

看看维基百科中的解释:

在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。

官方的解释总是不说人话,but–talk is cheap,show me the code:

# print_msg是外围函数
def print_msg():
    msg = "I'm closure"

    # printer是嵌套函数
    def printer():
        print(msg)

    return printer


# 这里获得的就是一个闭包
closure = print_msg()
# 输出 I'm closure
closure()

msg是一个局部变量,在print_msg函数执行之后应该就不会存在了。但是嵌套函数引用了这个变量,将这个局部变量封闭在了嵌套函数中,这样就形成了一个闭包。

结合这个例子再看维基百科的解释,就清晰明了多了。闭包就是引用了自有变量的函数,这个函数保存了执行的上下文,可以脱离原本的作用域独立存在。
有关python闭包更详细的解释参考:python中的闭包
下面来看看Python中的装饰器。

2 简单装饰器

一个普通的装饰器一般是这样:

import functools


def log(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print('call %s():' % func.__name__)
        print('args = {}'.format(*args))
        return func(*args, **kwargs)

    return wrapper

这样就定义了一个打印出方法名及其参数的装饰器。
调用之:

@log
def test(p):
    print(test.__name__ + " param: " + p)
    
test("I'm a param")
print(test.__name__)

输出:

call test():
args = I'm a param
test param: I'm a param
test

装饰器在使用时,用了@语法,让人有些困扰。其实,装饰器只是个方法,与下面的调用方式没有区别:

def test(p):
    print(test.__name__ + " param: " + p)

wrapper = log(test)
wrapper("I'm a param")

@语法只是将函数传入装饰器函数,并无神奇之处。

值得注意的是@functools.wraps(func),这是python提供的装饰器。它能把原函数的元信息拷贝到装饰器里面的 func 函数中。函数的元信息包括docstring、name、参数列表等等。可以尝试去除@functools.wraps(func),你会发现test.__name__的输出变成了wrapper。

3 带参数的装饰器

装饰器允许传入参数,一个携带了参数的装饰器将有三层函数,如下所示:

import functools

def log_with_param(text):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            print('call %s():' % func.__name__)
            print('args = {}'.format(*args))
            print('log_param = {}'.format(text))
            return func(*args, **kwargs)

        return wrapper

    return decorator
    
@log_with_param("param")
def test_with_param(p):
    print(test_with_param.__name__)

看到这个代码是不是又有些疑问,内层的decorator函数的参数func是怎么传进去的?和上面一般的装饰器不大一样啊。

其实道理是一样的,将其@语法去除,恢复函数调用的形式一看就明白了:

# 传入装饰器的参数,并接收返回的decorator函数
decorator = log_with_param("param")
# 传入test_with_param函数
wrapper = decorator(test_with_param)
# 调用装饰器函数
wrapper("I'm a param")

输出结果与正常使用装饰器相同:

call test_with_param():
args = I'm a param
log_param = param
test_with_param

至此,装饰器这个有点费解的特性也没什么神秘了。

装饰器这一语法体现了Python中函数是第一公民,函数是对象、是变量,可以作为参数、可以是返回值,非常的灵活与强大。

Python中引入了很多函数式编程的特性,需要好好学习与体会。

4 传参数给被装饰的函数

from functools import wraps

def dec(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        kwargs['name'] = 'no'
        kwargs['value'] = 'v'
        return func(*args, **kwargs)
    return wrapper

@dec
def foo(p1, p2, p3, name, value, p4='a'):
    print('p1: {}, p2: {}, p3:{}, name: {}, value:{}, p4: {}'.format(
        p1, p2, p3, name, value, p4
    ))


# p1,p2, p3是位置参数
# name,value 是关键字参数
# p4 默认参数

foo('a', 'b', 'c')
foo('a1', 'b1', 'c1', name = 'name')

主要关注如果装饰器会修改被装饰函数的参数时,如何去定义被装饰的函数

5 类作为装饰器

基于前面的使用函数作为装饰器的理解,将类作为装饰器时需要保证以下几点

  • 类的实例是可调用的
  • 类需要一个地方讲被装饰的函数传入到类的实例里

第一条可以通过__call__实现,第二条可以通过__init__实现。

class Profiled:
    def __init__(self, func):
        # wraps(func)(self)
        self.func = func

    def __call__(self, *args, **kwargs):
        print("call")
        return self.func(*args, **kwargs)
  
def add(x, y):
    return x + y
#手动模拟装饰器的过程
add = Profiled(add)
result = add(1, 2)
print(result)

执行结果:

call
3

似乎再用语法糖包装一下就可以实现将类作为装饰器来使用了。

from functools import wraps

class Profiled:

    def __init__(self, func):
        # wraps(func)(self)
        self.func = func

    def __call__(self, *args, **kwargs):
        print("call")
        return self.func(*args, **kwargs)
    
@Profiled
def add(x, y):
    return x + y

result = add(1, 2)
print(result)

运行结果同上。
上面将类作为装饰器使用,看起来都很正确,似乎就是这样使用?但是这种方法还有一些别的问题,最重要的是同样的问题,可以在代码中使用:

print(add.__name__)

发现无法输出__name__,根据约定使用装饰器不能改变函数的__name__属性,因此我们还需要改进将类作为装饰器的使用方法。
函数的属性发生变化,在之前是使用@wrap(func),同理使用类作为装饰器也有类似的方法的来实现。正确使用类作为装饰器使用的方法如下:

from functools import wraps

class Profiled:

    def __init__(self, func):
        wraps(func)(self)
        # self.func = func

    def __call__(self, *args, **kwargs):
        print("call")
        return self.__wrapped__(*args, **kwargs)
    
@Profiled
def add(x, y):
    return x + y

print(add.__name__)
result = add(1, 2)
print(result)

执行结果:

add
call
3

发现结果是预期的了。
根据Python官网文档, wraps(func)(self)本质是为类实例增加一个__wrapped__作为类实例属性,所以self.__wrapped__本质就是add函数。

6 普通装饰器装饰类成员函数

有关更高级的用法参考:python 装饰器,普通装饰函数修饰类函数

7 装饰类的装饰器

有关更高级的用法参考:Python 修饰类的装饰器

8 装饰器类应用于类的成员函数

有关更高级的用法参考:Python装饰器17-将装饰器类应用于类的成员函数

参考文章:

  1. 理解Python装饰器(Decorator)
  2. 如何理解Python装饰器? - 刘志军的回答 - 知乎
  3. Python装饰器16-正确使用装饰器类
  4. Python装饰器18-传参数给被装饰函数
  5. Python装饰器15-开始使用类作为装饰器
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值