美文网首页
Python 装饰器最佳实践

Python 装饰器最佳实践

作者: 阙馨妍子 | 来源:发表于2020-07-02 12:54 被阅读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中,函数是一类对象,这意味着:

    • 函数是对象,它们可以被引用,传递给变量并从其他函数返回。
    • 可以在另一个函数中定义函数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装饰器用于自定义类属性的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
    

    在这个类中:

    • .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 装饰器最佳实践

          本文链接:https://www.haomeiwen.com/subject/rfzlqktx.html