Python装饰器:详解(附带示例和用例)
Python装饰器在Python中是一个非常有用的构造。在Python中使用装饰器,我们可以通过将函数包装在另一个函数中来修改函数的行为。装饰器使我们能够编写更清晰的代码并共享功能。本文是一个教程,不仅介绍如何使用装饰器,还介绍如何创建装饰器。
先决知识
了解Python中的装饰器需要一些背景知识。下面,我列出了一些您应该已经熟悉的概念,以便理解本教程。如果需要,我还提供了可以复习这些概念的资源链接。
基本Python
这个主题是一个更中级/高级的主题。因此,在尝试学习之前,您应该已经熟悉Python的基本知识,例如数据类型、函数、对象和类。
您还应该理解一些面向对象的概念,如获取器、设置器和构造函数。如果您对Python编程语言不熟悉,这里有一些链接:some resources to get you started。
函数是一等公民
除了基本的Python知识外,您还应该了解Python中的这个更高级的概念。函数和Python中的几乎所有其他内容(如int
或string
)一样,都是对象。因为它们是对象,您可以对它们做一些事情,包括:
- 您可以将一个函数作为参数传递给另一个函数,就像将
string
或int
作为函数参数一样。 - 函数也可以像返回其他
string
或int
值一样,由其他函数返回。 - 函数可以存储在变量中。
实际上,函数对象和其他对象之间唯一的区别是函数对象包含魔术方法__call__()
。
希望到目前为止,您对先决知识感到满意。我们可以开始讨论主要主题了。
什么是Python装饰器?
Python装饰器只是一个接受函数作为参数并返回传入的函数的修改版本的函数。换句话说,如果函数foo作为参数接受函数bar
并返回另一个函数baz
,那么函数foo就是一个装饰器。
函数baz
是bar
的修改版本,因为在baz
的函数体中调用了函数bar
。但是,在调用bar
之前和之后,baz
可以做任何事情。这是一段冗长的话;下面是一些代码来说明这种情况:
# Foo是一个装饰器,它接受另一个函数bar作为参数
def foo(bar):
# 在这里,我们创建了baz,bar的修改版本
# baz将调用bar,但可以在函数调用之前和之后做任何事情
def baz():
# 在调用bar之前,我们打印一些内容
print("Something")
# 然后我们通过进行函数调用来运行bar
bar()
# 然后我们在运行bar之后打印一些其他内容
print("Something else")
# 最后,foo返回baz,bar的修改版本
return baz
如何在Python中创建装饰器?
为了说明如何在Python中创建和使用装饰器,我将用一个简单的例子来说明。在这个例子中,我们将创建一个日志记录器装饰器函数,每次运行被装饰的函数时,它都会记录该函数的名称。
首先,我们创建了装饰器函数。装饰器以func
作为参数。 func
是我们要装饰的函数。
def create_logger(func):
# 函数体在这里
def modified_func():
print(“调用: “, func.__name__)
func()
def create_logger(func):
def modified_func():
print(“调用: “, func.__name__)
func()
return modified_function
我们创建了一个装饰器。create_logger函数是一个简单的装饰器函数示例。它接受一个被装饰的函数func,并返回另一个函数modified_func。modified_func首先记录func的名称,然后调用func。使用装饰器的方法是使用@语法将装饰器应用到函数上。例如,在我们的脚本中调用say_hello(),输出应该是以下文本:
调用: say_hello
Hello, World!
但是@create_logger做了什么呢?实际上,它将装饰器应用到say_hello函数上。为了更好地理解它的作用,下面的代码与在say_hello之前放置@create_logger会达到相同的结果。
def say_hello():
print(“Hello, World!”)
say_hello = create_logger(say_hello)
换句话说,使用装饰器的一种方法是显式调用装饰器,将函数作为参数传递,就像我们在上面的代码中所做的那样。另一种更简洁的方法是使用@语法。
在本节中,我们介绍了如何创建Python装饰器。
稍微复杂的例子
上面的例子是一个简单的情况。还有稍微复杂一些的例子,比如被装饰的函数接受参数的情况。另一个更复杂的情况是当你想要装饰整个类时。我将在这里介绍这两种情况。
当函数接受参数时
当你要装饰的函数接受参数时,修改后的函数应该接收这些参数,并在最终调用未修改的函数时将它们传递过去。如果这听起来令人困惑,让我用foo-bar术语来解释一下。
回顾一下,foo是装饰器函数,bar是我们要装饰的函数,baz是装饰过的bar。在这种情况下,bar将接收参数,并在调用baz时将它们传递给baz。以下是一个代码示例,以加深概念:
def foo(bar):
def baz(*args, **kwargs):
# 在这里可以做一些操作
___
# 然后我们调用bar,传入args和kwargs
bar(*args, **kwargs)
# 在这里也可以做一些操作
___
return baz
如果*args和**kwargs看起来陌生,它们只是指向位置参数和关键字参数的指针。
需要注意的是,baz可以访问这些参数,并且可以在调用bar之前对参数进行一些验证。
一个例子是如果我们有一个装饰器函数ensure_string
,它确保传递给它装饰的函数的参数是一个字符串; 我们可以这样实现:
def ensure_string(func):
def decorated_func(text):
if type(text) is not str:
raise TypeError('argument to ' + func.__name__ + ' must be a string.')
else:
func(text)
return decorated_func
我们可以这样装饰say_hello
函数:
@ensure_string
def say_hello(name):
print('Hello', name)
然后我们可以使用以下代码测试代码:
say_hello('John') # 应该正常运行
say_hello(3) # 应该抛出异常
结果应该产生以下输出:
Hello John
Traceback (most recent call last):
File "/home/anesu/Documents/python-tutorial/./decorators.py", line 20, in say hello(3) # should throw an exception
File "/home/anesu/Documents/python-tu$ ./decorators.pytorial/./decorators.py", line 7, in decorated_func raise TypeError('argument to + func._name_ + must be a string.')
TypeError: argument to say hello must be a string. $0
如预期的那样,脚本成功打印了“Hello John”,因为“John”是一个字符串。当尝试打印“Hello 3”时,由于“3”不是一个字符串,它引发了一个异常。 ensure_string
装饰器可用于验证需要字符串参数的任何函数。
装饰一个类
除了装饰函数,我们还可以装饰类。当你将一个装饰器添加到一个类时,装饰的方法会替换类的构造函数/initiator方法(__init__)。
回到foo-bar,假设foo是我们的装饰器,Bar是我们要装饰的类,那么foo将装饰Bar.__init__。如果我们想在实例化类型为Bar
的对象之前做任何操作,这将非常有用。
这意味着以下代码
def foo(func):
def new_func(*args, **kwargs):
print('Doing some stuff before instantiation')
func(*args, **kwargs)
return new_func
@foo
class Bar:
def __init__(self):
print("In initiator")
等价于
def foo(func):
def new_func(*args, **kwargs):
print('Doing some stuff before instantiation')
func(*args, **kwargs)
return new_func
class Bar:
def __init__(self):
print("In initiator")
Bar.__init__ = foo(Bar.__init__)
实际上,使用这两种方法定义的类Bar的对象实例化将给出相同的输出:
Doing some stuff before instantiation
In initiator
Python中的装饰器示例
虽然你可以定义自己的装饰器,但Python中已经内置了一些。下面是您可能在Python中遇到的一些常见装饰器:
@staticmethod
静态方法用于在类上指示其装饰的方法是静态方法。静态方法是可以在不需要实例化类的情况下运行的方法。在以下代码示例中,我们创建一个带有静态方法bark
的类Dog
。
class Dog:
@staticmethod
def bark():
print('Woof, woof!')
现在可以通过以下方式访问bark
方法:
Dog.bark()
运行代码将产生以下输出:
Woof, woof!
正如我在如何使用装饰器的部分中提到的,装饰器有两种使用方式。其中一种是使用更简洁的@
语法。另一种方法是调用装饰器函数,将要装饰的函数作为参数传递进去。上述代码的效果与下面的代码相同:
class Dog:
def bark():
print('Woof, woof!')
Dog.bark = staticmethod(Dog.bark)
我们仍然可以以相同的方式使用bark
方法
Dog.bark()
并且会产生相同的输出
Woof, woof!
正如您所看到的,第一种方法更加简洁,甚至在开始阅读代码之前就很明显该函数是静态函数。因此,在剩余的示例中,我将使用第一种方法。但请记住第二种方法是一种替代方法。
@classmethod
这个装饰器用于指示它修饰的方法是一个类方法。类方法与静态方法相似,两者在被调用之前都不需要实例化类。
然而,主要区别是类方法可以访问类属性,而静态方法不能。这是因为Python在调用类方法时自动将类作为第一个参数传递给它。为了在Python中创建一个类方法,我们可以使用classmethod
装饰器。
class Dog:
@classmethod
def what_are_you(cls):
print("I am a " + cls.__name__ + "!")
要运行代码,我们只需调用该方法而不实例化类:
Dog.what_are_you()
输出结果为:
I am a Dog!
@property
property装饰器用于将一个方法标记为属性访问器。回到我们的Dog示例,让我们创建一个方法来获取Dog的名字。
class Dog:
# 创建一个接受狗的名字参数的构造方法
def __init__(self, name):
# 创建一个私有属性name
# 双下划线使得属性成为私有属性
self.__name = name
@property
def name(self):
return self.__name
现在我们可以像访问普通属性一样访问狗的名字
# 创建一个类的实例
foo = Dog('foo')
# 访问name属性
print("The dog's name is:", foo.name)
运行代码的结果将是
The dog's name is: foo
@property.setter
property.setter装饰器用于为属性创建一个setter方法。要使用@property.setter
装饰器,您需要将property
替换为要为其创建setter的属性的名称。例如,如果要为名为foo的属性的方法创建一个setter,您的装饰器将是@foo.setter
。以下是一个Dog示例来说明:
class Dog:
# 创建一个带有狗名参数的构造函数
def __init__(self, name):
# 创建一个私有属性name
# 双下划线使属性变为私有
self.__name = name
@property
def name(self):
return self.__name
# 创建一个用于name属性的setter
@name.setter
def name(self, new_name):
self.__name = new_name
为了测试setter,我们可以使用以下代码:
# 创建一个新的狗
foo = Dog('foo')
# 修改狗的名字
foo.name = 'bar'
# 将狗的名字打印到屏幕上
print("狗的新名字是:", foo.name)
运行代码将产生以下输出:
狗的新名字是: bar
装饰器在Python中的重要性
现在我们已经介绍了装饰器是什么,并且您已经看到了一些装饰器的示例,我们可以讨论装饰器在Python中的重要性。装饰器有几个重要性。我列举了其中一些如下:
- 它们实现了代码的可重用性:在上面给出的
logging
示例中,我们可以在任何想要的函数上使用@create_logger。这使我们能够在所有函数上添加日志功能,而无需为每个函数手动编写。 - 它们允许您编写模块化的代码:再次回到日志示例,使用装饰器,您可以将核心函数(在本例中为
say_hello
)与其他所需功能(在本例中为日志记录)分开。 - 它们增强了框架和库的功能:装饰器广泛用于Python框架和库中,以提供附加功能。例如,在Flask或Django等Web框架中,装饰器用于定义路由、处理身份验证或将中间件应用于特定视图。
最后的话
装饰器非常有用;您可以使用它们来扩展函数而不改变其功能。当您想要测量函数的性能、在调用函数时记录日志、在调用函数之前验证参数或在运行函数之前验证权限时,这非常有用。一旦您理解了装饰器,您将能够以更清晰的方式编写代码。
接下来,您可能想阅读我们关于tuples和using cURL in Python的文章。