Python 装饰器最佳实践
Python 的 Decorator
在使用上和Java/C#的Annotation
很相似,就是在方法名前面加一个@XXX
注解来为这个方法装饰一些东西。但是,Java/C#的Annotation
很让人望而却步,要使用它,需要先了解一堆Annotation
的类库文档,让人感觉就是在学另外一门语言。
而Python使用了一种相对于Annotation
来说非常优雅的方法,这种方法不需要我们去掌握Annotation
的各种类库规定,完全就是语言层面的玩法:一种函数式编程(“描述我们想干什么,而不是描述我们要怎么去实现”的编程方式)的技巧。
装饰器基础知识
函数对象
要理解decorator,首先必须理解函数在Python中的作用。这有重要的影响。让我们用一个简单的例子来看看为什么:
def shout(word='yes'):
return word.capitalize()
print(shout())
# outputs : 'Yes'
scream = shout
# 注意,我们不使用括号:我们没有调用函数,而是将`shout`函数赋给变量`scream`,
# 这意味着您可以从`scream`中调用`shout`:
print(scream())
# outputs : 'Yes'
# 不仅如此,这还意味着您可以删除`shout`,并且该函数仍然可以从`scream`调用
del shout
try:
print(shout())
except NameError as e:
print(e)
#outputs: "name 'shout' is not defined"
print(scream())
# outputs: 'Yes'
Python函数的另一个有趣的特性是 可以在另一个函数中定义它们!
def talk():
# 您可以在`talk`中动态定义一个函数: ...
def whisper(word='yes'):
return word.lower()
# ... 并立即使用!
print(whisper())
# 您每次调用`talk`时都会定义`whisper`,然后`whisper`在`talk`中被调用。
talk()
# outputs: "yes"
# 但是`talk`之外不存在`whisper`:
try:
print(whisper())
except NameError as e:
print(e)
# outputs : "name 'whisper' is not defined"
函数引用
你已经知道函数是对象,因此,函数:
- 可以给变量赋值
- 可以在另一个函数中定义
这意味着一个函数可以返回另一个函数。看一下!
def getTalk(kind='shout'):
# 我们动态地定义函数
def shout(word='yes'):
return word.capitalize()
def whisper(word='yes'):
return word.lower()
# 然后我们返回其中一个
if kind == 'shout':
# 我们不用'()'。我们没有调用函数;相反,我们返回函数对象
return shout
else:
return whisper
# 获取函数并将其赋值给变量
talk = getTalk()
# 你可以看到`talk`在这里是一个函数对象:
print(talk)
#outputs : <function shout at 0xb7ea817c>
print(talk())
# outputs : 'Yes'
# 你甚至可以直接使用它:
print(getTalk('whisper')())
# outputs : 'yes'
既然你可以返回一个函数,那么你也可以将函数作为参数传递给另一个函数:
def doSomethingBefore(func):
print('I do something before then I call the function you gave me')
print(func())
doSomethingBefore(shout)
# outputs:
# I do something before then I call the function you gave me
# Yes
现在您已经具备了了解装饰器的一切条件。在Python中,函数是一类对象,这意味着:
- 函数是对象,它们可以被引用,传递给变量并从其他函数返回。
- 可以在另一个函数中定义函数
inner function
,也可以将其作为参数传递给另一个函数。
手动实现装饰器
您已经看到函数与Python中的任何其他对象一样,现在让我们手动实现一个装饰器,来看一下Python装饰器的魔力。
# 装饰器是期望另一个函数作为参数的函数
def my_decorator(my_func):
# 在内部,decorator动态地定义了一个函数:wrapper。
# 这个函数将被封装在原始函数上,这样它就可以在原始函数之前和之后执行代码。
def my_wrapper():
# 在调用原始函数之前,将需要执行的代码放在这里
print('Before the function runs')
# 调用这里的函数(使用括号)
my_func()
# 将您希望在调用原始函数后执行的代码放在这里
print('After the function runs')
# 此时,`my_func`还没有被执行。
# 我们返回刚刚创建的`my_wrapper`函数。
# `my_wrapper`包含`my_func`函数和要执行的前后代码。
return wrapper
# 现在假设您创建了一个不想再做任何修改的函数
def my_func():
print('I am a stand alone function, don’t you dare modify me')
my_func()
# outputs: I am a stand alone function, don't you dare modify me
# 只要将它传递给装饰器,它就会动态地将它包装在您想要的任何代码中,并返回一个准备使用的新函数:
my_func_decorator = my_decorator(my_func)
my_func_decorator()
# outputs:
# Before the function runs
# I am a stand alone function, don't you dare modify me
# After the function runs
现在,您可能希望每次调用my_func
时,my_func_decorator
会被调用。这很简单,只需用my_decorator
返回的函数覆盖my_func
my_func = my_decorator(my_func)
my_func
# outputs:
# Before the function runs
# I am a stand alone function, don’t you dare modify me
# After the function runs
装饰器揭秘
使用装饰器语法实现前面的例子:
@my_func_decorator
def my_another_func():
print('Leave me alone')
my_another_func()
# outputs:
# Before the function runs
# Leave me alone
# After the function runs
是的,就是这么简单。根据我们前面的铺垫,您应该一下就能理解装饰器的语法,@decorator
只是一个快捷方式:
my_another_func = my_decorator(my_another_func)
decorator
只是decorator设计模式的python变体。Python中嵌入了一些经典的设计模式来简化开发(比如迭代器、生成器,感兴趣的同学可以看一下我前面关于迭代器和生成器的文章:Python中的三个“黑魔法”与“骚操作”。
嵌套装饰器
当然,装饰器也可以嵌套:
def hello(func):
def wrapper():
print("Hello")
func()
return wrapper
def welcome(func):
def wrapper():
print("Welcome")
func()
return wrapper
def say():
print("Good")
say = hello(welcome(say))
say()
# outputs:
# Hello
# Welcome
# Good
# 使用Python decorator语法:
@hello
@welcome
say()
# outputs:
# Hello
# Welcome
# Good
设置decorator
的顺序很重要:decorator
按照它们被列出的顺序执行。
带参装饰器
我们还可以装饰一个带有参数的函数。我们可以在包装器函数wrapper
中使用*args
和**kwargs
接收这些参数。
# 你只需要让`wrapper`传递参数:
def say(func):
def wrapper(*args, **kwargs):
func(*args, **kwargs)
return wrapper
# 因为在调用`decorator`返回的函数时,调用的是包装器`wrapper`,所以向包装器传递参数将让包装器将参数传递给修饰的函数
@say
def greet(name):
print("Hello {}".format(name))
greet("xiaojing")
# outputs: Hello xiaojing
装饰器高手进阶
现在,你已经掌握了装饰器的概念和装饰器的基本用法,可以高兴地离开了,或者你也可以留下多动会脑子,看看装饰器的高级用途。
Introspection
在Python中,自省是指对象在运行时了解其自身属性的能力。例如,函数知道自己的name和doc。
print(greet.__name__)
# outputs: wrapper
但我们期望输出greet
,而不是函数被装饰后丢失函数原始的信息。要解决这个问题,decorator
应该在wrapper
上使用@functools.wrapper
包装器函数,它将保留关于原始函数的信息。
import functools
import time
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start_time = time.perf_counter()
value = func(*args, **kwargs)
end_time = time.perf_counter()
run_time = end_time - start_time
print("Finished {} in {} s".format(repr(func.__name__), round(run_time, 3)))
return value
return wrapper
@timer
def doubled_and_add(num):
res = sum([i*2 for i in range(num)])
print("Result : {}".format(res))
doubled_and_add(100000)
doubled_and_add(1000000)
# outputs:
# Result : 9999900000
# Finished ‘doubled_and_add’ in 0.0119 s
# Result : 999999000000
# Finished ‘doubled_and_add’ in 0.0897 s
装饰类
在类上使用装饰器有两种不同的方法。装饰类的方法或装饰整个类。
内置类装饰器
Python中内置的一些常用装饰器是@classmethod
、@staticmethod
和@property
。@classmethod
和@staticmethod
装饰器用于在类名称空间内定义未连接到该类的特定实例的方法。@property
装饰器用于自定义类属性的getter
和setter
方法。
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value >= 0:
self._radius = value
else:
raise ValueError("Radius must be positive")
@property
def area(self):
return self.pi() * self.radius**2
def cylinder_volume(self, height):
return self.area * height
@classmethod
def unit_circle(cls):
"""工厂方法 创建一个半径为1的圆"""
return cls(1)
@staticmethod
def pi():
return 3.1415926535
在这个类中:
-
.cylinder_volume()
是一个普通方法。 -
.radius
是一个可变的属性:它可以被设置为不同的值。但是,通过定义setter方法,我们可以进行一些错误验证,来确保它不会被设置为负数。 -
.area
是一个不可变的属性:没有.setter()方法的属性是不能更改的。 -
.unit_circle()
是类方法。它不局限于一个特定的圆实例。类方法通常用作工厂方法,可以创建类的特定实例。 -
.pi()
是静态方法,它并不真正依赖于Circle类。静态方法可以在实例或类上调用。
控制台测试:
>>> c = Circle(5)
>>> c.radius
5
>>> c.area
78.5398163375
>>> c.radius = 2
>>> c.area
12.566370614
>>> c.area = 100
AttributeError: can't set attribute
>>> c.cylinder_volume(height=4)
50.265482456
>>> c.radius = -1
ValueError: Radius must be positive
>>> c = Circle.unit_circle()
>>> c.radius
1
>>> c.pi()
3.1415926535
>>> Circle.pi()
3.1415926535
装饰方法
Python的一个妙处是方法和函数实际上是一样的。惟一的区别是,方法期望它们的第一个参数是对当前对象(self)的引用,在这里,我们使用上面刚刚创建的计时器装饰器,我们还是举个例子简单过一下:
class Calculator:
def __init__(self, num):
self.num = num
@timer
def doubled_and_add(self):
res = sum([i * 2 for i in range(self.num)])
print("Result : {}".format(res))
c = Calculator(10000)
c.doubled_and_add()
# outputs:
# Result : 99990000
# Finished 'doubled_and_add' in 0.001 s
装饰整个类
@timer
class Calculator:
def __init__(self, num):
self.num = num
import time
time.sleep(2)
def doubled_and_add(self):
res = sum([i * 2 for i in range(self.num)])
print("Result : {}".format(res))
c = Calculator(100)
# outputs: Finished 'Calculator' in 2.001 s
装饰类并不装饰它的方法。在这里,@timer只测量实例化类所需的时间。
带参数的装饰器
注意,带参数的装饰器和上面提到的带参装饰器可不是一回事儿。带参装饰器是装饰带参数的函数的装饰器,这个参数是函数的参数,通过包装器传递给函数。而带参数的装饰器是带有参数的装饰器,这个参数是装饰器自身的参数,是不是有点晕了,别急,我们一起看下例子就懂了:
def repeat(*args_, **kwargs_):
def inner_function(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for _ in range(args_[0]):
func(*args, **kwargs)
return wrapper
return inner_function
@repeat(4)
def say(name):
print(f"Hello {name}")
say("World")
# outputs:
# Hello World
# Hello World
# Hello World
# Hello World
有状态的装饰器
我们可以使用一个装饰器来跟踪状态。作为一个简单的示例,我们将创建一个decorator来计算函数被调用的次数。
def count_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
wrapper.num_calls += 1
print(f"Call {wrapper.num_calls} of {func.__name__!r}")
return func(*args, **kwargs)
wrapper.num_calls = 0
return wrapper
@count_calls
def say():
print("Hello!")
say()
say()
say()
say()
print(say.num_calls)
# outputs:
# Call 1 of 'say'
# Hello!
# Call 2 of 'say'
# Hello!
# Call 3 of 'say'
# Hello!
# Call 4 of 'say'
# Hello!
# 4
对函数的调用数量存储在包装器函数上的函数属性num_calls
中。
类装饰器
注意,和刚才带参数的装饰器和带参装饰器类似,类装饰器和装饰类的装饰器也是完全不同的两个概念。装饰类的装饰器是用来装饰类的方法和整个类的装饰器,是对类的装饰。而类装饰器是用类来作为函数的装饰器,类本身作为装饰器对函数进行装饰。我在说What?这次我自己都要绕晕了,还是借代码来翻译程序吧,例子一看您就明白了。
class CountCalls:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.num_calls = 0
def __call__(self, *args, **kwargs):
self.num_calls += 1
print(f"Call {self.num_calls} of {self.func.__name__!r}")
return self.func(*args, **kwargs)
@CountCalls
def say():
print("Hello!")
say()
say()
say()
say()
print(say.num_calls)
# outputs:
# Call 1 of 'say'
# Hello!
# Call 2 of 'say'
# Hello!
# Call 3 of 'say'
# Hello!
# Call 4 of 'say'
# Hello!
# 4
维护状态的最佳方法是使用类。如果我们想使用class作为装饰器,则需要将func
在其.__init__()
方法中作为参数。此外,该类必须是可调用的,以便它可以代表被装饰的函数。对于可调用的类,我们需要实现特殊的.__call__()
方法。
带参数的基于类的装饰器
我保证这是最后一个!解释不动了,直接上代码,代码是对程序语言最好的解释语言。
class ClassDecorator(object):
def __init__(self, arg1, arg2):
print("Arguements of decorator %s, %s" % (arg1, arg2))
self.arg1 = arg1
self.arg2 = arg2
def __call__(self, func):
functools.update_wrapper(self, func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@ClassDecorator("arg1", "arg2")
def print_args(*args):
for arg in args:
print(arg)
print_args(1, 2, 3)
# outputs:
# Arguements of decorator arg1, arg2
# 1
# 2
# 3
总结
Python 的 Decorator
是想要对一个已有的模块做一些“修饰工作”,所谓修饰工作就是想给现有的模块加上一些小装饰(一些小功能,这些小功能可能好多模块都会用到),但又不让这个小装饰(小功能)侵入到原有的模块中的代码里去,上面我们用了大量的例子来说明了这一点。
推荐你们几篇比较不错的英文文章(锦上添花,不读也可):
- Python decorator best practice, using a class vs a function:stack overflow上的一个问答,比较精彩,值得一读。
- Primer on Python Decorators – Real Python:这是挺全的一个pytohn decorator教程
- The best explanation of Python decorators I’ve ever seen. (An archived answer from StackOverflow.):这篇比较偏重基础,适合没接触过decorator的小白阅读。
- Decorators in Python:这篇理论相对较少,需要一定的python基础。
终于完事儿了,恭喜你们,太牛了!如果本文对大家有帮助,欢迎大家对本文点赞收藏评论或关注我的主页,我会不定期更新当下主流技术文章。