Python 装饰器入门(下)

继续上次的进度:https://www.cnblogs.com/flashBoxer/p/9847521.html

正文:

装饰类

在类中有两种不通的方式使用装饰器,第一个和我们之前做过的函数非常相似:在类的方法上应用。这也是当时引入装饰器的原因之一

一些常用的装饰器已经内置到python中,像@classmethod @staticmethod @property。这三个装饰器我们之前都介绍过,这段就不翻译了(打字手酸,偷懒下)

下面的Circle 类使用了@classmethod @staticmethod和@property三个装饰器

class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def radius(self):
        """Get value of radius"""
        return self._radius

    @radius.setter
    def radius(self, value):
        """Set radius, raise error if negative"""
        if value >= 0:
            self._radius = value
        else:
            raise ValueError("Radius must be positive")

    @property
    def area(self):
        """Calculate area inside circle"""
        return self.pi() * self.radius**2

    def cylinder_volume(self, height):
        """Calculate volume of cylinder with circle as base"""
        return self.area * height

    @classmethod
    def unit_circle(cls):
        """Factory method creating a circle with radius 1"""
        return cls(1)

    @staticmethod
    def pi():
        """Value of π, could use math.pi instead though"""
        return 3.1415926535

在这个类中
    .cylinder_volume()是一个常规函数
    .radius是一个可变属性:它可以被设置不同的值.然而通过定义setter方法,我们可以做一些判断来确保它不会被设置成一个没有意义的负数,.radius作为属性访问,不使用括号
    .area 是一个不可变的属性:没有.setter()方法的属性是无法更改的,即使它被定义为一个方法,它也被作为不需要括号的属性来使用。
    .unit_circle() 是一个类方法。它不被绑定到Circle的实例上.类方法通常用在工厂模式,用来创建类的特殊实例
    .pi() 是一个静态方法.除了命名空间外它不依赖Circle类。静态方法可以在实例或类上调用。

 

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

让我们定义一个类,在这个类中,我们会用到前面的@debug和@timer装饰器:

from decorators import debug, timer

class TimeWaster:
    @debug
    def __init__(self, max_num):
        self.max_num = max_num

    @timer
    def waste_time(self, num_times):
        for _ in range(num_times):
            sum([i**2 for i in range(self.max_num)])

看一下结果:

>>> tw = TimeWaster(1000)
Calling __init__(<time_waster.TimeWaster object at 0x7efccce03908>, 1000)
'__init__' returned None

>>> tw.waste_time(999)
Finished 'waste_time' in 0.3376 secs

另外一种方式是在整个类上使用装饰器.这里有个Python3.7中的dataclasses方法用例:

from dataclasses import dataclass

@dataclass
class PlayingCard:
    rank: str
    suit: str

语法的类似于函数装饰器。在上面的例子中,也可以通过PlayingCard = dataclass(PlayingCard)来实现。

类装饰器的一种简单用法是作为元类方式的替代.在两种情况下,你都在动态的改变一个类的定义

类的装饰器和函数的装饰器语法接近,不同的是装饰器需要接收一个类而不是一个函数作为参数.事实上,上面的装饰器都可以作用于类,但当你这么用的时候,你可能得不到预期的结果。下面将@timer装饰器应用到一个类

from decorators import timer

@timer
class TimeWaster:
    def __init__(self, max_num):
        self.max_num = max_num

    def waste_time(self, num_times):
        for _ in range(num_times):
            sum([i**2 for i in range(self.max_num)])

@timer只是TimeWaster = timer(TimeWaster)的缩写


在这里@timer只能显示类实例化需要的时间

>>> tw = TimeWaster(1000)
Finished 'TimeWaster' in 0.0000 secs

>>> tw.waste_time(999)
>>>

在后面会有一个正确的类装饰器的示例@singleton。它保证一个类只有一个实例


嵌套的装饰器


可以将多个装饰器叠加到一个函数上

from decorators import debug, do_twice

@debug
@do_twice
def greet(name):
    print(f"Hello {name}")

运行的顺序会按照叠加的顺序, @debug 调用 @do_twice @do_twice 调用greet(),或者debug(do_twice(greet()))  

>>> greet("Eva")
Calling greet('Eva')
Hello Eva
Hello Eva
'greet' returned None        

更改@debug和@do_twice的顺序:

from decorators import debug, do_twice

@do_twice
@debug
def greet(name):
    print(f"Hello {name}")    

在这种情况下,@do_twice也会被应用到@debug中: 

>>> greet("Eva")
Calling greet('Eva')
Hello Eva
'greet' returned None
Calling greet('Eva')
Hello Eva
'greet' returned None       

带参数的装饰器

 在需要传参给你的装饰器是这个例子会非常有用。例如,@do_twice可以扩展到@repeat(num_times)装饰器.然后,可以将执行的被装饰函数的次数作为参数给出。      
可以这么做:

@repeat(num_times=4)
def greet(name):
    print(f"Hello {name}")      
        
>>> greet("World")
Hello World
Hello World
Hello World
Hello World        

       
考虑下如何实现这个功能

到目前为止,写在@后面写的名字引用一个可以被另外一个函数调用的函数对象,需要repeat(num_times=4)来返回一个函数对象,这个对象可以被作为装饰器,幸运的是,我们已经知道如何返回函数!一般来说,需要以下内容:

def repeat(num_times):
    def decorator_repeat(func):
        ...  # Create and return a wrapper function
    return decorator_repeat

通常,装饰器创建并返回一个内部包装函数,所以完整地写出这个例子会给你一个内部函数

def repeat(num_times):
    def decorator_repeat(func):
        @functools.wraps(func)
        def wrapper_repeat(*args, **kwargs):
            for _ in range(num_times):
                value = func(*args, **kwargs)
            return value
        return wrapper_repeat
    return decorator_repeat

例子看起来有点乱,但我们只是添加了一个def来接收参数,这个装饰器语法我们之前处理过多次.让我们从最里面的函数开始:

def wrapper_repeat(*args, **kwargs):
    for _ in range(num_times):
        value = func(*args, **kwargs)
    return value

wrapper_repeat()函数接收任意参数,并放回被装饰函数的值,func(). 这个包装函数还包括了被装饰函数num_times的循环 ,除了必须要使用外部参数num_times外,和之前看到的装饰器函数没有什么不同,

再走一步,你就会发现装饰器函数:

def decorator_repeat(func):
    @functools.wraps(func)
    def wrapper_repeat(*args, **kwargs):
        ...
    return wrapper_repeat

decorator_repeat()和我们之前写的装饰器函数非常像,除了他的名字不同,因为我们为最外层的函数保留了基础名称repeat(),这个是用户要调用的函数。

最外层返回装饰器函数的引用

def repeat(num_times):
    def decorator_repeat(func):
        ...
    return decorator_repeat

在repeat()中有一些细节:

        将decorator_repeat()作为一个内部函数意味着repeat()将引用一个函数对象-decotator_repeat.之前,我们用没有括号的repeat来引用函数对象.定义带有参数的装饰器,就需要添加括号
        
        num_times参数看起来没有在repeat()本身中使用,但是通过传递num_times,会创建一个闭包,来存储num_times的值,直到wrapper_repeat()使用它为止。
    
一切就绪后,让我们看看结果:

@repeat(num_times=4)
def greet(name):
    print(f"Hello {name}")

>>> greet("World")
Hello World
Hello World
Hello World
Hello World


这是我们想要的结果

Both Please, But Never Mind the Bread

稍微注意下.你可以把装饰器同时定义为带参数或者不带参数.你可能不需要这样,但更有灵活性也不错

前面已经看到,当装饰器需要参数的时候,需要有一个额外的外部函数,困难在于,代码需要知道装饰器是否被调用了,是否有参数

因为只有在没有参数的情况下调用装饰器时才会直接传递装饰的函数,这个函数必须是可选参数.意味着装饰器参数必须要友关键字指定,可以使用特殊的*,也就是说,下面的参数都是关键字

def name(_func=None, *, kw1=val1, kw2=val2, ...):  # 1
    def decorator_name(func):
        ...  # Create and return a wrapper function.

    if _func is None:
        return decorator_name                      # 2
    else:
        return decorator_name(_func)               # 3

_func参数是一个标记,提示装饰器被调用的时候是否有参数
    1.如果name调用的时候没有传参,被装饰函数会被作为_func传入.如果有参数传入,_func会被置为None,一些关键字参数可能已不再是默认值, 参数列表中的*表示其余参数不能作为位置参数调用。

    2.装饰器可以传参调用,返回一个装饰器函数,它可以读取和返回一个函数
    
    3.装饰器不可以传参调用,会只将装饰器应用到函数上

改造下之前的@repeat装饰器

def repeat(_func=None, *, num_times=2):
    def decorator_repeat(func):
        @functools.wraps(func)
        def wrapper_repeat(*args, **kwargs):
            for _ in range(num_times):
                value = func(*args, **kwargs)
            return value
        return wrapper_repeat

    if _func is None:
        return decorator_repeat
    else:
        return decorator_repeat(_func)

和之前的对比,唯一的变化是在末尾添加了_func参数和if-else。
这些例子表明,@repeat现在可以在有或没有参数的情况下使用:

@repeat
def say_whee():
    print("Whee!")

@repeat(num_times=3)
def greet(name):
    print(f"Hello {name}")

默认情况num_times的值是2

>>> say_whee()
Whee!
Whee!

>>> greet("Penny")
Hello Penny
Hello Penny
Hello Penny

有状态的装饰器

有时候,可以跟踪状态的装饰器也是很有用的.一个简单的例子,我们会创建一个统计函数调用次数的装饰器

注意:在教程的前面,我们讨论了基于给定参数返回值的纯函数.有状态的装饰器正好相反,返回值取决于当前状态以及给定的参数。

在下一节中,您将看到如何使用类来保持状态。但在简单的情况下,也可以使用函数属性:

import functools

def count_calls(func):
    @functools.wraps(func)
    def wrapper_count_calls(*args, **kwargs):
        wrapper_count_calls.num_calls += 1
        print(f"Call {wrapper_count_calls.num_calls} of {func.__name__!r}")
        return func(*args, **kwargs)
    wrapper_count_calls.num_calls = 0
    return wrapper_count_calls

@count_calls
def say_whee():
    print("Whee!")

状态——函数的调用次数——存储在包裹函数(wrapper_count_calls)的函数属性.num_calls中。下面是使用它的效果:

>>> say_whee()
Call 1 of 'say_whee'
Whee!

>>> say_whee()
Call 2 of 'say_whee'
Whee!

>>> say_whee.num_calls
2

类装饰器

典型的维护状态的方式是使用类。在本节中,将看到如何重写@count_calls的例子来实现类装饰器

回想一下,装饰器语法@my_decorator只是func = my_decorator(func)一种方便快捷的用法.因此,如果my_decorator是一个类,需要在它的.__init__方法中接收func作为一个参数.而且,这个类需要是可以被调用的,这样它就可以替代装饰器函数了

如果需要一个类可以被调用,要实现.__call__方法(看示例:https://www.cnblogs.com/flashBoxer/p/9774064.html)

class Counter:
    def __init__(self, start=0):
        self.count = start

    def __call__(self):
        self.count += 1
        print(f"Current count is {self.count}")

.__call__方法每次运行都会尝试调用一个类的实例:

>>> counter = Counter()
>>> counter()
Current count is 1

>>> counter()
Current count is 2

>>> counter.count
2

因此,实现类装饰器需要实现.__init__和.__call__

import functools

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_whee():
    print("Whee!")

.__init__方法必须可以存储一个函数的引用和能够做一些必要的初始化. 调用.__call__方法来替代装饰器函数.它做的和我们之前的 wrapper()函数基本一样,注意,这里使用functools.update_wrapper()函数,而不是@functools.wraps

这个@CountCalls装饰器的工作原理与前一节相同:

>>> say_whee()
Call 1 of 'say_whee'
Whee!

>>> say_whee()
Call 2 of 'say_whee'
Whee!

>>> say_whee.num_calls
2

 

更多现实中的例子


我们已经学到了很多(看了下翻译的行数量,已经1K+了,确实很多),已经学会如何创建各种各样的装饰师,把我们的新知识应用到创建更多的示例中,这些示例在现实中可能非常有用。


代码降速,重新访问


我们之前实现的@slow_down一直是保持sleep 1秒.现在你知道了如何给装饰器添加参数,因此,让我们来重写@slow_down,使用一个可选的rate参数来控制它的sleep时间:

import functools
import time

def slow_down(_func=None, *, rate=1):
    """Sleep given amount of seconds before calling the function"""
    def decorator_slow_down(func):
        @functools.wraps(func)
        def wrapper_slow_down(*args, **kwargs):
            time.sleep(rate)
            return func(*args, **kwargs)
        return wrapper_slow_down

    if _func is None:
        return decorator_slow_down
    else:
        return decorator_slow_down(_func)

我们使用  Both Please, But Never Mind the Bread  这里的样例来让@slow_down有参数和没有参数时都可调用,countdown()函数现在在每次计数之间休眠2秒:

@slow_down(rate=2)
def countdown(from_number):
    if from_number < 1:
        print("Liftoff!")
    else:
        print(from_number)
        countdown(from_number - 1

和前面一样,你最好自己写写,跑下看看结果

>>> countdown(3)
3
2
1
Liftoff!

创建单例模式

单例模式是一个只有一个实例的类.在Python经常使用的单例对象包括None,True和False.可以使用is来比较,像我们之前在Both Please的章节中:

if _func is None:
    return decorator_name
else:
    return decorator_name(_func)

is只对完全相同实例的对象返回True。下面的@singleton装饰器将类的第一个实例存储为属性,从而将类转换为单例对象。之后创建实例只是返回已经存储的实例:

import functools

def singleton(cls):
    """Make a class a Singleton class (only one instance)"""
    @functools.wraps(cls)
    def wrapper_singleton(*args, **kwargs):
        if not wrapper_singleton.instance:
            wrapper_singleton.instance = cls(*args, **kwargs)
        return wrapper_singleton.instance
    wrapper_singleton.instance = None
    return wrapper_singleton

@singleton
class TheOne:
    pass

这个类装饰器和我们的函数装饰器基本一样.唯一不同的地方在于使用cls代替了fun来表示这是一个类装饰器

看下运行结果:

>>> first_one = TheOne()
>>> another_one = TheOne()

>>> id(first_one)
140094218762280

>>> id(another_one)
140094218762280

>>> first_one is another_one
True


很明显,first_one确实与另一个实例完全相同。

缓存返回值

装饰器可以提供很方便的缓存和记忆机制.作为一个例子,我们来看看斐波那契数列的递归定义:

from decorators import count_calls

@count_calls
def fibonacci(num):
    if num < 2:
        return num
    return fibonacci(num - 1) + fibonacci(num - 2)

实现很简单,性能很糟糕

>>> fibonacci(10)
<Lots of output from count_calls>
55

>>> fibonacci.num_calls
177

为了计算第10个斐波那契数,你实际上只需要计算前面的斐波那契数,但是这个实现需要177次计算。更糟糕的是:斐波纳契数列(20)需要21891次计算,第30次需要270万次计算。这是因为代码一直在重新计算已知的斐波那契数。

通常的解决方案是使用for循环和查找表来实现斐波那契数。但是,简单的计算缓存也可以做到这一点:

import functools
from decorators import count_calls

def cache(func):
    """Keep a cache of previous function calls"""
    @functools.wraps(func)
    def wrapper_cache(*args, **kwargs):
        cache_key = args + tuple(kwargs.items())
        if cache_key not in wrapper_cache.cache:
            wrapper_cache.cache[cache_key] = func(*args, **kwargs)
        return wrapper_cache.cache[cache_key]
    wrapper_cache.cache = dict()
    return wrapper_cache

@cache
@count_calls
def fibonacci(num):
    if num < 2:
        return num
    return fibonacci(num - 1) + fibonacci(num - 2)

缓存作为查找表工作,所以现在fibonacci()只执行一次计算:

>>> fibonacci(10)
Call 1 of 'fibonacci'
...
Call 11 of 'fibonacci'
55

>>> fibonacci(8)
21

注意,在对fibonacci(8)的最后调用中,没有进行新的计算,因为fibonacci(10)已经计算了第8个fibonacci数。
在标准库中,提供了@functools.lru_cache。

这个装饰器比上面的例子要具备更多特性.我们应该使用@functools.lru_cache来代替我们自己写的缓存装饰器

import functools

@functools.lru_cache(maxsize=4)
def fibonacci(num):
    print(f"Calculating fibonacci({num})")
    if num < 2:
        return num
    return fibonacci(num - 1) + fibonacci(num - 2)

maxsize参数指定缓存了多少次调用。默认值是128,但是可以指定maxsize=None来缓存所有函数调用。但是,请注意,如果正在缓存许多很大的对象,这可能会导致内存问题。

可以使用.cache_info()方法查看缓存的执行情况,并在需要时进行调优。在我们的示例中,我们设定一个小maxsize来查看从缓存中删除元素的效果:

>>> fibonacci(10)
Calculating fibonacci(10)
Calculating fibonacci(9)
Calculating fibonacci(8)
Calculating fibonacci(7)
Calculating fibonacci(6)
Calculating fibonacci(5)
Calculating fibonacci(4)
Calculating fibonacci(3)
Calculating fibonacci(2)
Calculating fibonacci(1)
Calculating fibonacci(0)
55

>>> fibonacci(8)
21

>>> fibonacci(5)
Calculating fibonacci(5)
Calculating fibonacci(4)
Calculating fibonacci(3)
Calculating fibonacci(2)
Calculating fibonacci(1)
Calculating fibonacci(0)
5

>>> fibonacci(8)
Calculating fibonacci(8)
Calculating fibonacci(7)
Calculating fibonacci(6)
21

>>> fibonacci(5)
5

>>> fibonacci.cache_info()
CacheInfo(hits=17, misses=20, maxsize=4, currsize=4)

添加单元信息

下面的示例与前面的Registering Plugins示例有点类似,因为它不会真正改变被装饰函数的行为。相反,它只是将unit添加为函数属性:

def set_unit(unit):
    """Register a unit on a function"""
    def decorator_set_unit(func):
        func.unit = unit
        return func
    return decorator_set_unit

下面的示例根据圆柱体的半径和高度(以厘米为单位)来计算体积:

import math

@set_unit("cm^3")
def volume(radius, height):
    return math.pi * radius**2 * height   
    
这个.unit函数属性是可以访问的:    
>>> volume(3, 5)
141.3716694115407

>>> volume.unit
'cm^3' 

注意,可以使用函数注释实现类似的功能: 

import math

def volume(radius, height) -> "cm^3":
    return math.pi * radius**2 * height   

但是,由于注释用于类型提示,因此很难将注释和静态类型检查相结合。   
    
在连接到一个能够在单位间转换的库,单位可以变得更加强大和有趣.pip install pint,  您可以将体积转换为立方英寸或加仑: 

>>> import pint
>>> ureg = pint.UnitRegistry()
>>> vol = volume(3, 5) * ureg(volume.unit)

>>> vol
<Quantity(141.3716694115407, 'centimeter ** 3')>

>>> vol.to("cubic inches")
<Quantity(8.627028576414954, 'inch ** 3')>

>>> vol.to("gallons").m  # Magnitude
0.0373464440537444  


你还可以修改装饰器来直接返回一个Pint数量.数量是通过与单位相乘得到的,在pint中,units必须只能在UnitRegistry中查询.这里注册用来存储函数属性来避免命名空间混乱

def use_unit(unit):
    """Have a function return a Quantity with given unit"""
    use_unit.ureg = pint.UnitRegistry()
    def decorator_use_unit(func):
        @functools.wraps(func)
        def wrapper_use_unit(*args, **kwargs):
            value = func(*args, **kwargs)
            return value * use_unit.ureg(unit)
        return wrapper_use_unit
    return decorator_use_unit

@use_unit("meters per second")
def average_speed(distance, duration):
    return distance / duration


使用@use_unit装饰器,转换单位实际上是很容易

>>> bolt = average_speed(100, 9.58)
>>> bolt
<Quantity(10.438413361169102, 'meter / second')>

>>> bolt.to("km per hour")
<Quantity(37.578288100208766, 'kilometer / hour')>

>>> bolt.to("mph").m  # Magnitude
23.350065679064745

验证JSON

让我们看最后一个用例。快速看下Flask路由的管理程序:

@app.route("/grade", methods=["POST"])
def update_grade():
    json_data = request.get_json()
    if "student_id" not in json_data:
        abort(400)
    # Update database
    return "success!"

这里我们确保key student_id是请求的一部分.虽然验证有效,但它实际上并不属于函数本身.另外,可能还有其他使用相同验证的路由。因此,让我们Don't repeat yourself,来使用装饰器抽象出任何不必要的逻辑,下面的@validate_json装饰器会完成这个工作:

from flask import Flask, request, abort
import functools
app = Flask(__name__)

def validate_json(*expected_args):                  # 1
    def decorator_validate_json(func):
        @functools.wraps(func)
        def wrapper_validate_json(*args, **kwargs):
            json_object = request.get_json()
            for expected_arg in expected_args:      # 2
                if expected_arg not in json_object:
                    abort(400)
            return func(*args, **kwargs)
        return wrapper_validate_json
    return decorator_validate_json

在上面的代码中,装饰器采用了一个可变长度列表作为参数,这样我们就可以传递尽可能多的字符串参数,每个参数都代表一个用于验证JSON数据的键:

    1.json的keys列表作为参数传递给装饰器
    2.包裹函数验证JSON数据中出现的每个预期键

然后,路由管理程序可以关注其真正的业务级别——因为它可以安全地假设JSON数据是有效的:

@app.route("/grade", methods=["POST"])
@validate_json("student_id")
def update_grade():
    json_data = request.get_json()
    # Update database.
    return "success!"

 

 

结束语:翻译就到这里吧,这篇文章的作者对装饰器的理解很是深入,文章很长,翻起来确实花了不少时间。文中如果有翻译不稳妥的地方,请留言给我。最后老铁们如果觉得对理解python的装饰器有帮助,右下角点个赞吧,结尾附上原文地址:https://realpython.com/primer-on-python-decorators/

 

posted @ 2018-10-29 22:53  丁壮  阅读(811)  评论(0编辑  收藏  举报