Python 装饰器


2021年8月23日, Learn eTutorial
2039

在本教程中,您将通过简单的示例学习什么是Python装饰器,以及如何创建和使用它们。在开始本教程之前,让我揭示一个事实,装饰器很难理解!但我们向您保证,在最后您将毫无疑问地掌握这个主题。

什么是 Python 中的装饰器

装饰器是元编程的一部分,因为它们通过在编译时为现有函数或类添加额外功能来增强 Python 代码。

装饰器可以定义为一个函数,它接受另一个函数并以某种方式修改后一个函数的行为,而无需显式修改实际的源代码。

在开始本教程之前,我建议您先熟悉
函数式编程的基本主题,以便您可以毫无困难地掌握装饰器的概念。下面简要介绍了这些基础知识。

Python中的对象概念

Python 是一门优美的语言,它广泛地利用了对象的概念。在 Python 中,几乎一切都是对象。变量、常量、函数甚至类都是对象。下面的例子展示了函数如何作为对象工作。

示例:函数作为对象

def func1():
    print('Welcome to Learn eTutorials')

func1()

func2= func1
func2() 

输出

Welcome to Learn eTutorials
Welcome to Learn eTutorials

当上面的代码执行时,func1 和 func2 都会产生相同的输出。这表明 func1 和 func2 引用的是同一个函数对象。

嵌套函数

Python 允许在一个函数内部定义另一个函数,通常称为嵌套函数或内部函数。下面是一个嵌套函数的简单示例。

def OuterFunction(msg): 

                def Innerfunction():  
                                print(msg)
                InnerFunction() 
           
OuterFunction('Welcome to Learn eTutorials')  

将函数作为参数传递给另一个函数

在 Python 中,函数是一等公民。这意味着函数可以作为参数传递,赋值给变量,或者用作返回值,就像 Python 中的其他对象(字符串、列表、元组等)一样。

示例:函数作为参数

def say_hello(name):
    print('Hello',name )

def say_bye(name):
    print('Bye',name )
    

def Greet(func, name):
    return func(name)

Greet(say_hello,'TOM')
Greet(say_bye,'JERRY') 

输出

Hello TOM
Bye JERRY

在上面的代码中,say_hello 和 say_bye 是两个常规函数,它们接受一个字符串(name)作为参数。另一方面,Greet() 函数接受两个参数,一个是函数,另一个是字符串变量。在所有定义的函数中,Greet() 是一个高阶函数,因为它接受另一个函数作为其参数。

从另一个函数返回函数

在 Python 中,可以从另一个函数返回一个函数。在这种情况下,函数被视为返回值。下面的例子从 OuterFunction 返回 InnerFunction。

Closure Strucuture

闭包结构

这里我们返回 InnerFunction 时没有带括号。这表示我们返回的是对 InnerFunction 的引用,这形成了一个闭包。请查看我们之前的教程以了解更多关于闭包的信息。

上述四个函数特性使得装饰器在 Python 中成为可能。

编写你的第一个装饰器

现在让我们从一个简单的例子开始,并尝试理解这个程序。

示例:第一个装饰器程序

def decor_func(func):
    def wrap_func():
        print( '<' * 32)
        func()
        print('>' * 32)
    return wrap_func

def welcome():
    print('\nWELCOME ALL TO LEARN ETUTORIALS\n')

greet=decor_func(welcome)
greet() 

解释

在上面的例子中,我们有一个名为 welcome 的常规函数,它不接受任何参数,该函数的目的是打印给定的消息“WELCOME ALL TO LEARN ETUTORIALS”。
现在假设您希望装饰函数 welcome(),同时又不想修改源代码。这可能吗?答案是肯定的。装饰器帮助您装饰一个函数而不触及源代码。

为了装饰 welcome(),我们定义了一个装饰器函数 decor_func,它接受一个参数,该参数是一个函数。在 decor_func() 内部,我们又定义了另一个函数,并将其命名为 wrap_func,它不接受任何参数。wrap_func() 调用作为参数传递给 decor_func 的函数,并将其 (func) 放置在旨在修改的额外功能之间。在我们的例子中,我们在 func 之前和之后都包含了 print()。最后返回 wrap 函数,从而成为一个闭包。

所谓的装饰发生在下面的语句中

greet = decor_func(welcome) 

函数 welcome() 被装饰,返回的函数被赋给一个名为 greet 的变量。greet() 函数调用将产生输出,输出将如下所示

输出

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

WELCOME ALL TO LEARN ETUTORIALS

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

从输出中可以很清楚地看到,我们像包装礼物一样装饰了函数。在这里,装饰器纯粹作为原始函数的包装器,保持其性质不变。从而增强了函数的美感。

语法糖

在计算机科学中,语法糖是编程语言中的一种语法,它使代码使用起来更美好、更甜。在 Python 中,声明装饰器的语法方式是使用 @ 符号。上面的代码可以更改如下

示例:语法糖

def decor_func(func):
    def wrap_func():
        print( '<' * 32)
        func()
        print('>' * 32)
    return wrap_func

@decor_func
def welcome():
    print('\nWELCOME ALL TO LEARN ETUTORIALS\n')

welcome() 

通过使用这样的语法糖,我们可以更清晰、更优雅地表达代码。这里,

@decor_func 

is equivalent to:

greet=decor_func(welcome)
greet() 

函数上的多个装饰器

Python 允许链接装饰器,从而能够在单个函数上使用多个装饰器。您唯一需要记住的是您希望应用于函数的装饰器的顺序。检查以下示例以了解堆叠装饰器。

#Stacked Decorators
def decor_func1(func):
    def wrap_func():
        print( '<' * 32)
        func()
        print('>' * 32)
    return wrap_func

def decor_func2(func):
    def wrap_func():
        print( 'XO' * 16)
        func()
        print('XO' * 16)
    return wrap_func

@decor_func1
@decor_func2
def welcome():
    print('\nWELCOME ALL TO LEARN ETUTORIALS\n')

#greet = decor_func(welcome)
#greet()
welcome() 

输出

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
XOXOXOXOXOXOXOXOXOXOXOXOXOXOXOXO

WELCOME ALL TO LEARN ETUTORIALS

XOXOXOXOXOXOXOXOXOXOXOXOXOXOXOXO
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

上面的程序包含两个装饰器函数,即 decor_func1decor_func2。这些装饰器函数按特定顺序堆叠,因此输出中遵循了该模式。最初,我们用 decor_func2 装饰函数 welcome,然后用 decor_func1 装饰。现在如果我们改变装饰函数的顺序,输出将会不同。观察下面程序的输出。

#Stacked Decorators
def decor_func1(func):
    def wrap_func():
        print( '<' * 32)
        func()
        print('>' * 32)
    return wrap_func

def decor_func2(func):
    def wrap_func():
        print( 'XO' * 16)
        func()
        print('XO' * 16)
    return wrap_func

@decor_func2
@decor_func1
def welcome():
    print('\nWELCOME ALL TO LEARN ETUTORIALS\n')

#greet = decor_func(welcome)
#greet()
welcome() 

输出

XOXOXOXOXOXOXOXOXOXOXOXOXOXOXOXO
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

WELCOME ALL TO LEARN ETUTORIALS

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
XOXOXOXOXOXOXOXOXOXOXOXOXOXOXOXO

因此,我们可以得出结论,在堆叠时,装饰器函数的顺序很重要

由于装饰器类似于普通函数,我们可以在装饰器上应用函数的所有特性。装饰器可以通过导入在其他函数甚至其他文件中使用。因此,像函数一样,装饰器也是可重用的。

装饰带参数的函数

当您观察到目前为止的程序时,您会注意到内部函数都保持为空。这意味着没有参数传递给内部函数。然而,在某些时候可能需要传递参数。下面是一个将两个数相除的示例,我们考虑除以零的可能性对其应用装饰器。

def check(func):
    def inner(x,y):
        print("Divide" ,x ,"by" ,y)
        if y == 0:
            print("Error: Division by zero is not allowed")
            return 
        return x / y
    return inner

@check
def division(a,b):
    return a/b

print(division(10,2)) 

输出

Divide 10 by 2
5.0

在这里,这个例子中的装饰器函数是 check,它接受函数作为其参数。内部函数也接受两个变量 xy。装饰器函数检查变量 y(即除法的分母部分)是否为零。如果不等于零,输出将如上所示,否则输出将如下所示

Divide 10 by 0
Error: Division by zero is not allowed
None

所以在这里我们创建的装饰器 check 最适合除法函数。然而,我们知道装饰器不限于任何单个函数;它也可以被其他函数使用。下面显示了一个用于测试执行时间的通用装饰器的简单示例

from time import time
def timetest(func):
    def wrapper(*args,**kwargs):
        start_time=time()
        result = func(*args,**kwargs)
        end_time=time()
        print("Elapsed Time: {}".format(end_time - start_time))
        return result
    return wrapper

@timetest
def pow(a,b):
    return a**b
print(pow(500,5))

@timetest
def avg(n):
    if n == 0:
        return 0
    else:
        sum=0
        for i in range(n+1):
             sum=sum+i
        return sum/n
print(avg(599999)) 

输出

Elapsed Time: 0.0
31250000000000
Elapsed Time: 0.0957489013671875
300000.0

在这个例子中,我们定义的两个函数是

  • pow() 用于计算一个数的幂
  • avg() 用于计算 n 个数的平均值

这两个函数都用一个名为 timetest 的通用函数装饰,该函数确定执行时间。在这里,当您观察时,您可以看到两个函数都向装饰器传递了不同数量的参数。即使这样,装饰器也能完美运行,您能猜出原因吗?

这是因为在我们的程序中,我们在内部函数 wrapper 中使用了 *args 和 **kwargs,这使得装饰器能够接受任意数量的位置参数和关键字参数。

Functools 和包装器

装饰器最终所做的只是用另一个函数包装或替换我们的函数。您可以通过尝试为上述程序打印函数名称来见证这一点,如下所示。

print(pow.__name__)
print(avg.__name__) 

输出将是

wrapper
wrapper

所以我们最终丢失了被传递的函数的信息。解决这个问题的一种方法是在内部函数中重置它们,但这不被认为是一种优雅的方法。

幸运的是,Python 有另一种方法,即我们可以利用 functools 模块,其中包含 functools.wraps。Wraps 是一个装饰器,它通过接收传递的函数来装饰内部函数,并将诸如名称、文档字符串、签名等属性复制到内部函数的属性中。

检查下面的程序,它展示了我们如何在不丢失信息的情况下装饰上面的例子。

from functools import wraps
from time import time
def timetest(func):
    @wraps(func)    
    def wrapper(*args,**kwargs):
        start_time=time()
        result = func(*args,**kwargs)
        end_time=time()
        print("Elapsed Time: {}".format(end_time - start_time))
        return result
    return wrapper

@timetest
def pow(a,b):
    return a**b
print(pow(500,5))

@timetest
def avg(n):
    if n == 0:
        return 0
    else:
        sum=0
        for i in range(n+1):
             sum=sum+i
        return sum/n
print(avg(599999))

print(pow.__name__)
print(avg.__name__) 

输出

Elapsed Time: 0.0
31250000000000
Elapsed Time: 0.09993958473205566
300000.0
pow
avg