Python 装饰器最佳实践

2020-07-02  本文已影响0人  阙馨妍子

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中,函数是一类对象,这意味着:

手动实现装饰器

您已经看到函数与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装饰器用于自定义类属性的gettersetter方法。

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

在这个类中:

控制台测试:

>>> 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是想要对一个已有的模块做一些“修饰工作”,所谓修饰工作就是想给现有的模块加上一些小装饰(一些小功能,这些小功能可能好多模块都会用到),但又不让这个小装饰(小功能)侵入到原有的模块中的代码里去,上面我们用了大量的例子来说明了这一点。
推荐你们几篇比较不错的英文文章(锦上添花,不读也可):

终于完事儿了,恭喜你们,太牛了!如果本文对大家有帮助,欢迎大家对本文点赞收藏评论或关注我的主页,我会不定期更新当下主流技术文章。

相关文章

上一篇下一篇

猜你喜欢

热点阅读