Python中的装饰器(decorator)

作者:无尘粉笔
链接:https://www.zhihu.com/question/271201015/answer/2387427580
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

装饰器(decorator)是Python一个进阶的用法,通常以如下形式出现,但是它到底怎么用呢?且看本文细细分解。

@decorator           # 装饰器
def self_defined_function():
    print("Self-defined function is called.")

1. 什么时候需要用装饰器?

试想你有一系列函数,例如如下所示的两个函数(分别计算和与积): 

def my_sum_function(*args):
    print("The sum is", sum(args))

def my_product_function(*args):
    res = 1
    for x in args:
        res *= x
    print("The product is", res)

my_sum_function(1, 2, 3, 4)
my_product_function(1, 2, 3, 4, 5)

现在,你需要为这一系列函数添加一个一样的功能,就是统计输入参数的个数,同时检查里面是否有0,那么低阶的解决方法就是为每一个函数单独添加部分代码,即:

def my_sum_function(*args):
    print("No. of input args is", len(args))                # 重复部分
    contain_zero = any([x == 0 for x in args])              # 重复部分
    print("Input arguments contain 0:", contain_zero)       # 重复部分

    print("The sum is", sum(args))

def my_product_function(*args):
    print("No. of input args is", len(args))                # 重复部分
    contain_zero = any([x == 0 for x in args])              # 重复部分
    print("Input arguments contain 0:", contain_zero)       # 重复部分

    res = 1
    for x in args:
        res *= x
    print("The product is", res)

my_sum_function(1, 2, 3, 4)
my_product_function(0, 1, 2, 3, 4, 5)

这样写的话,代码中存在大量重复的部分,有没有好一点的办法呢?也许你已经想到了,就是把添加的这部分功能封装成一个单独的函数,然后让每一个函数单独调用这个函数(嵌套调用),这样就能减少写代码过程中的“Ctrl+C和Ctrl+V”,即

def my_sum_function(*args):
    additional_function(*args)    # 嵌套调用

    print("The sum is", sum(args))

def my_product_function(*args):
    additional_function(*args)    # 嵌套调用

    res = 1
    for x in args:
        res *= x
    print("The product is", res)

def additional_function(*args):
    print("No. of input args is", len(args))
    contain_zero = any([x == 0 for x in args])
    print("Input arguments contain 0:", contain_zero)

my_sum_function(1, 2, 3, 4)
my_product_function(0, 1, 2, 3, 4, 5)

那有没有更pythonic的写法呢?这里就可以用到装饰器了,实现方法如下 

def a_decorator(f):
    def additional_function(*args):
        print("No. of input args is", len(args))
        contain_zero = any([x == 0 for x in args])
        print("Input arguments contain 0:", contain_zero)
        f(*args)
    return additional_function

@a_decorator
def my_sum_function(*args):
    print("The sum is", sum(args))

@a_decorator
def my_product_function(*args):
    res = 1
    for x in args:
        res *= x
    print("The product is", res)

my_sum_function(1, 2, 3, 4)
my_product_function(0, 1, 2, 3, 4, 5)

2. 装饰器的执行机制

现在我么已经知道了装饰器的使用场景:就是为函数定制化额外功能的时候,可是添加装饰器。使用装饰器可以使代码更加简洁。那么装饰器是如何工作的呢?我们以如下例子说明:

def a_decorator(f):                         # 函数作为参数被传入
    print(f"Function {f.__name__} is passed as the augument!")
    def additional_function(*args):         # 函数嵌套
        print("No. of input args is", len(args))
        contain_zero = any([x == 0 for x in args])
        print("Input arguments contain 0:", contain_zero)
        f(*args)    # 最终执行目标函数my_sum_function的地方
    return additional_function  # 函数作为返回对象

@a_decorator
def my_sum_function(*args):
    print("The sum is", sum(args))

my_sum_function(1, 2, 3, 4)

运行结果如下 

$ python demo.py
Function my_sum_function is passed as the augument!
No. of input args is 4
Input arguments contain 0: False
The sum is 10

在进一步解释运行机制之前,我们需要明确几个问题:

  • 一、在Python中,一切皆对象,包括函数。一个函数可以作为另一个函数的参数,一个函数也可以作为另一个函数的返回对象;
  • 二、如果在一个函数体中定义了另一个函数,称为函数嵌套,前者称为enclosing function,后者称为enclosed function或者nested function

根据运行结果,我们可以反推出装饰器的工作机制:

  • my_sum_function作为参数传入a_decorator函数,并开始执行,因此首先打印出“Function my_sum_function is passed as the augument!”
  • 随后a_decorator函数执行过程中返回了additional_function函数对象,然后开始执行additional_function(1, 2, 3, 4),于是打印出“No. of input args is 4”和“Input arguments contain 0: False”
  • 在additional_function函数嵌套调用了my_sum_function函数,因此最后打印“The sum is 10”

也就是说,上述例子如果不用@符号,和下面是完全等价的:

def a_decorator(f):
    print(f"Function {f.__name__} is passed as the augument!")
    def additional_function(*args):
        print("No. of input args is", len(args))
        contain_zero = any([x == 0 for x in args])
        print("Input arguments contain 0:", contain_zero)
        f(*args)
    return additional_function

def my_sum_function(*args):
    print("The sum is", sum(args))

a_decorator(my_sum_function)(1, 2, 3, 4)    # 注意理解这一行

3. 闭包

在上述例子中,对于a_decorator函数的设计使用了闭包(closure)的概念[1],即

  1. 一个函数中嵌套了另一个函数,如上述a_decorator函数中嵌套了additional_function函数
  2. enclosed function中直接使用了enclosing funcion中的参数,如上述additional_function函数中使用了a_decorator函数的参数f
  3. 最终返回enclosing funcion,如上述例子中最终返回了additional_function

满足以上三个条件的称为Python闭包。

4. 函数名被重写了怎么办?用functools.wraps!

在上述例子中,如我们print(my_sum_function.__name__),会发现:

def a_decorator(f):                         # 函数作为参数被传入
    print(f"Function {f.__name__} is passed as the augument!")
    def additional_function(*args):         # 函数嵌套
        print("No. of input args is", len(args))
        contain_zero = any([x == 0 for x in args])
        print("Input arguments contain 0:", contain_zero)
        f(*args)    # 最终执行目标函数my_sum_function的地方
    return additional_function  # 函数作为返回对象

@a_decorator
def my_sum_function(*args):
    print("The sum is", sum(args))

print(my_sum_function.__name__)
# output: additional_function

my_sum_function函数的名字被重写了,怎么解决这个问题呢,很简单,调用functools.wraps即可[2],即 

from functools import wraps

def a_decorator(f):                         # 函数作为参数被传入
    print(f"Function {f.__name__} is passed as the augument!")
    @wraps(f)                               # 注意这一行
    def additional_function(*args):         # 函数嵌套
        print("No. of input args is", len(args))
        contain_zero = any([x == 0 for x in args])
        print("Input arguments contain 0:", contain_zero)
        f(*args)    # 最终执行目标函数my_sum_function的地方
    return additional_function  # 函数作为返回对象

@a_decorator
def my_sum_function(*args):
    print("The sum is", sum(args))

print(my_sum_function.__name__)
# output: my_sum_function

5. 如何给装饰器传入参数?带参装饰器!

如果我想给装饰器再添加一个功能,就是把目标函数(my_sum_function)的计算结果存入本地文件,本地文件的文件名可以作为参数随时进行修改。这时候就需要用到带参装饰器了,那么带参装饰器如何实现呢?其实很简单,就是再嵌套一层,即

from functools import wraps
filename = "output.txt"

def a_decorator(filename):   
    print(filename)
    def inter_function(f):  # 第一层嵌套
        @wraps(f)
        def additional_function(*args): # 第二层嵌套       
            print("No. of input args is", len(args))
            contain_zero = any([x == 0 for x in args])
            print("Input arguments contain 0:", contain_zero)
            result = f(*args)    
            with open(filename, "w") as txt:
                txt.write(result)
                txt.close()
        return additional_function  
    return inter_function

@a_decorator(filename)  # 带参装饰器
def my_sum_function(*args):
    print("The sum is", sum(args))
    return str(sum(args))

my_sum_function(1, 2, 3)
# 执行完毕会在本地创建output.txt文件同时写入结果

你应该也已经注意到了,在调用functools.wraps时,@wraps(f)其实就是一个带参装饰器。

6. 类装饰器

除了函数可以作为装饰器,类也可以作为装饰器,这时候就需要类可以作为函数进行调用。如何实现把类作为函数使用呢?答案是__call__魔法函数,如

from functools import wraps

class A_decorater:

    def __init__(self) -> None:
        pass

    def __call__(self, f):
        @wraps(f)
        def additional_function(*args): 
            print("No. of input args is", len(args))
            contain_zero = any([x == 0 for x in args])
            print("Input arguments contain 0:", contain_zero)
            f(*args)    
        return additional_function  


@A_decorater()  # 千万注意这里括号不能少,因为A_decorater是类,A_decorater()是类的实例
def my_sum_function(*args):
    print("The sum is", sum(args))
    return str(sum(args))

my_sum_function(1, 2, 3)

如果要带参数的话,同样地需要再嵌套一层,即 

from functools import wraps
filename = "output.txt"

class A_decorater:

    def __init__(self) -> None:
        pass

    def __call__(self, filename):
        print(filename)
        def inter_function(f):
            @wraps(f)
            def additional_function(*args): 
                print("No. of input args is", len(args))
                contain_zero = any([x == 0 for x in args])
                print("Input arguments contain 0:", contain_zero)
                result = f(*args)    
                with open(filename, "w") as txt:
                    txt.write(result)
                    txt.close()
            return additional_function  
        return inter_function

a_decorater = A_decorater()    # 不可避免,还是要先生成一个实例

@a_decorater(filename)
def my_sum_function(*args):
    print("The sum is", sum(args))
    return str(sum(args))

my_sum_function(1, 2, 3)

参考

  1. ^Python装饰器、闭包 9. Python 装饰器、闭包 — openstack 1.0 documentation
  2. ^Python 函数装饰器 Python 函数装饰器 | 菜鸟教程



作者:无尘粉笔
链接:https://www.zhihu.com/question/271201015/answer/2387427580
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值