Python装饰器

 

现在,我们来定义一个函数,fight。这个函数需要3个参数,color,time,o,分别是颜色、时间、某个对象。

def fight(color,t,o):
    print(f'我们出生在{color}方阵营')
    print(f'敌军还有{t}秒到达战场')
    print(f'{o}出击')

 

玩过moba游戏的都知道这是游戏开头的语音播报,我们就以这个举例。

现在我有一个需求,让你计算一下这个函数的运行时间,但是不能修改函数的内部代码。你会怎么做?

 

 

 

方案1

单凭你的直觉,不要想太多,你肯定立马就能想到,这也太简单了,只需要在函数的前后计算一下时间,然后算一下间隔就可以了。

# encoding:utf-8
import time
def fight(color,t,o):
    print(f'我们出生在{color}方阵营')
    print(f'敌军还有{t}秒到达战场')
    print(f'{o}出击')



start = time.time()
time.sleep(2)

fight('红色','27','小许')

end = time.time()
print(end-start)

  

 

 

(由于案例的函数太简单,运行时间几乎是瞬间完成,所以用time模块睡了2秒)

 

这样就得到了函数的运行时间,很简单吧。

 

但是,实际工作中,需求肯定不是这么简单的,我们这个方法对单独一个函数这样操作没问题,实际工作中,我们可能要给大量的函数添加时间计算的功能,你总不能,每调用一次函数,就手动敲这些代码吧,作为程序员,不够优雅与方便,这种方案不可取。


方案2

既然有重复代码了,那我就用函数来解决重复的部分。经过一阵思考后:
 
在当前代码外面包裹一层函数不就可以了,同时把fight需要的参数,放在外层函数的形参里,就能传值了。
 
# encoding:utf-8
import time

def fight(color,t,o):
    print(f'我们出生在{color}方阵营')
    print(f'敌军还有{t}秒到达战场')
    print(f'{o}出击')

def wrapper(color,t,o):
    start = time.time()
    time.sleep(2)

    fight(color,t,o)

    end = time.time()
    print(end-start)

wrapper("红色","27","小许")

评价:这下我们把fight函数的参数写活了,让他用外部的wrapper函数的参数,这样就方便多了

但是问题,依旧存在

 

 

假如fight函数后续还需要其他参数呢?我们当初写这个函数的时候,只是初步传入了一些参数,后续的改动中,很可能需要加入新的参数。这是很常见的。

就像你的老板,永远会给你提新的需求。需求是无止境的。-------------参数个数无法确定

 

方案3

 

这次你发狠了,行行行,要参数是吧,我就给你参数。

你想到了之前学习的可变位置参数、可变关键字参数,这里,你终于用上了

# encoding:utf-8
import time

def fight(color,t,o):
    print(f'我们出生在{color}方阵营')
    print(f'敌军还有{t}秒到达战场')
    print(f'{o}出击')

def wrapper(*args,**kwargs):
   start = time.time()
   time.sleep(2)
fight(
*args,*kwargs)
end
= time.time() print(end-start) wrapper("红色","27","小许")

 

 

 

wrapper调用的时候,会先把传入的3个实参:红色、27、小许,打包为元祖(“红色”,“27”,“小许”)。

 

内层函数fight调用的时候,会把元祖给打散,依次拿到红色、27、小许这3个参数,这样就可以实现了随便传参的目的,

 

评价:这一次,我们的方案终于很强了,可以随便传参,而且还不修改被装饰对象fight的代码。太强了!

 

但是有个问题,我们写的计时用的装饰器,只能为fight函数服务,其他函数享用不到,怎么让其他函数享用到呢。

就需要把fight函数给写活了。所以,我们还要继续优化我们的方案!

 

方案4:

怎么把函数wrapper写活呢,让它能为其他任何函数服务呢?

经过思考,我们发现:

 

想把它写活,就需要把函数当做变量用。

# encoding:utf-8
import time

def fight(color,t,o):
    print(f'我们出生在{color}方阵营')
    print(f'敌军还有{t}秒到达战场')
    print(f'{o}出击')

func = fight
def wrapper(*args,**kwargs):
   start = time.time()
   time.sleep(2)
   func(*args,*kwargs)
   end = time.time()
   print(end-start)


wrapper("红色","27","小许")

 

 我们在wrapper外部定义一个变量 func,用它来接受fight的值,注意fight不要加括号,加了括号就等于调用函数了,我们这里只要fight的函数内存地址

 

评价:这一次,我们使用一个变量func来接收被装饰函数的内存地址,大大增加了灵活性。问题依旧存在,

 

每次装饰某个函数,都需要先手动敲一下func=被装饰函数,然后再调用装饰函数,不够优雅,甚至是麻烦。

 

TIPS:

函数加了括号,等于调用了;单独的函数名,得到的是函数的内存地址。

 

方案5:

我们继续思考,之前为了给fight函数增加功能,我们在外部包了一层函数wrapper。这次我们还想给wrapper增加功能,那就如法炮制吧。

# encoding:utf-8
import time

def fight(color,t,o):
    print(f'我们出生在{color}方阵营')
    print(f'敌军还有{t}秒到达战场')
    print(f'{o}出击')

def outter():
    func = fight
    def wrapper(*args,**kwargs):
        start = time.time()
        time.sleep(2)
        func(*args,*kwargs)
        end = time.time()
        print(end-start)
    return wrapper  # 把wrapper的内存地址返回去
res = outter()   #调用outter函数,返回wrapper的内存地址

res("红色","27","小许")
print(res)  # res的值是wrapper的内存地址

 

 

 

这一次,我们在outter函数内部放了一个变量func来接收被被装饰函数fight的内存地址。

 

我们用res来接收outter()的返回值,outter()的返回值是wrapper的内存地址。

 

所以,我们使用  res("红色","27","小许")     调用函数,其实就是被装饰函数func在执行

 

不过,还是有些小问题,因为我们把 func = fight放在outter函数内部了,这相当于把函数写死了,所以为了增加灵活性,我们需要把func放在outter的参数里

# encoding:utf-8
import time

def fight(color,t,o):
    print(f'我们出生在{color}方阵营')
    print(f'敌军还有{t}秒到达战场')
    print(f'{o}出击')

def outter(func):

    def wrapper(*args,**kwargs):
        start = time.time()
        time.sleep(2)
        func(*args,*kwargs)
        end = time.time()
        print(end-start)
    return wrapper  # 把wrapper的内存地址返回去

res = outter(fight)   #调用outter函数,返回wrapper的内存地址

res("红色","27","小许")

print(res)  # res的值是wrapper的内存地址

 

以后使用outter函数的时候,只需要给它传入一个函数的函数名(也就是函数的内存地址),不要带括号。

 

方案6

 

现在,还有一个问题,被我们忽略了 ,如果被装饰函数有返回值呢?

返回值是很重要的,丢失了返回值,是很严重的后果。

我们再来优化一下,很简单,用一个变量来接收被装饰函数的返回值,再把这个变量return出来就可以了。

 

# encoding:utf-8
import time

def fight(color,t,o):
    print(f'我们出生在{color}方阵营')
    print(f'敌军还有{t}秒到达战场')
    print(f'{o}出击')

def outter(func):

    def wrapper(*args,**kwargs):
        start = time.time()
        time.sleep(2)
        result = func(*args,*kwargs)
        end = time.time()
        print(end-start)
        return result
    return wrapper  # 把wrapper的内存地址返回去

res = outter(fight)   #调用outter函数,返回wrapper的内存地址

res("红色","27","小许")

print(res)  # res的值是wrapper的内存地址

 

 

 

现在,还有一个小问题,我们每次使用outter的时候,都需要先进行赋值:fight = outter(fight),把被装饰函数fight的函数名放在参数里,总是写这一行代码,也不够优雅。

因此,Python为我们提供了一个简便的操作,就是语法糖

我们只需要在被装饰函数的头部,写上 @outter 就可以了

@outter
def fight(color,t,o):
    print(f'我们出生在{color}方阵营')
    print(f'敌军还有{t}秒到达战场')
    print(f'{o}出击')
    return "你真是个小天才"

 

 

这就是装饰器!在不修改被装饰函数的代码的情况下,给被装饰函数增加扩展功能

 

 

 

 

@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)

 

有没有更好的办法呢 ?  那就是 装饰器闪亮登场。

                                  
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)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

原文出处:

https://www.zhihu.com/question/271201015/answer/2387427580

https://www.zhihu.com/question/271201015/answer/3178585157

 

posted @ 2024-08-14 17:20  chengxuyonghu  阅读(7)  评论(0编辑  收藏  举报