在本教程中,您将通过简单的示例学习什么是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。
这里我们返回 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_func1 和 decor_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,它接受函数作为其参数。内部函数也接受两个变量 x 和 y。装饰器函数检查变量 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,这使得装饰器能够接受任意数量的位置参数和关键字参数。
装饰器最终所做的只是用另一个函数包装或替换我们的函数。您可以通过尝试为上述程序打印函数名称来见证这一点,如下所示。
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