Python装饰器, since 2022-02-10

(2022.02.24 Thur 难点和问题:装饰器参数和可选参数,其中_func的作用;@functools.wraps的作用;*args和**kwargs的作用;类作为装饰器的其他应用;python实现单例模式的多种方式和线程使用;用装饰器计数和实现单例模式的最终返回之前有个置0操作,该操作的目的是什么?)
(2022.03.26 Sat 装饰器的特性之一:在被装饰的函数定义之后立即执行)

(2022.02.10 Thur)
本节翻译自realpython的decorator页面。

Decorator的引入

在Python中,函数是一类对象(first-class object),这意味着函数可以被传递,并作为参数来使用,如同stringintlist等对象。比如

def say_hello(name):
    return f"Hello {name}"

def greet_bob(greeter_func):
    return greeter_func("Bob")

函数say_hello接受一个参数,并返回Hello。函数great_bob接收一个函数,并返回该函数调用Bob的返回结果。因此可以用如下方法调用say_hello函数

>>> greet_bob(say_hello)
Hello Bob

注意到在传递say_hellogreet_bob时,say_hello并没有加括号,这代表着传参过程传递的是对函数的引用(reference to the function),say_hello在传递时并未被执行。而greet_bob函数指定了参数,因此是正常调用。

内部函数Inner function

在函数内部定义的函数称为内部函数Inner function。

def parent():
    print("Printing from the parent() function")

    def first_child():
        print("Printing from the first_child() function")

    def second_child():
        print("Printing from the second_child() function")

    second_child()
    first_child()

调用parent函数结果如下

>>> parent()
Printing from the parent() function
Printing from the second_child() function
Printing from the first_child() function

内部函数的定义顺序不影响输出结果,只有调用顺序影响输出。此外,该案例中的内部函数只有在parent函数执行时才被定义并调用,他们如同局部变量一样在parent函数的内部存在。单独调用first_childsecond_child将没有响应。

Traceback (most recent call last):
  File "", line 1, in 
NameError: name 'first_child' is not defined

从函数返回函数

Python中函数的返回值可以是函数,i.e., 对函数的引用。比如

def parent(num):
    def first_child():
        return "Hi, I am Emma"

    def second_child():
        return "Call me Liam"

if num == 1:
    return first_child
elif num == 2:
    return second_child
else:
    return "specify a number between 1 and 2"

注意,在num是1或2时,返回结果是函数名,不包括括号,说明返回结果是对函数的引用。作为对比,调用函数时加括号,如first_child()则引用函数的运行结果。

简单decorator

(2022.02.11 Fri wellness day)
先看这个例子

def my_decorator(func):
    def wrapper():
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper

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

say_whee = my_decorator(say_whee)

调用say_whee时,返回如下内容

>>> say_whee
.wrapper at 0x000001E6C0EF1CA0>
>>> say_whee()
Something is happening before the function is called.
Whee!
Something is happening after the function is called.

在这个例子中,这一步被称作装饰decoration

say_whee = my_decorator(say_whee)

在装饰之后,say_whee引用了名为wrapper的内部函数,而wrapper包裹了(wrapper)/引用了say_whee初始函数。

在这里我们看到装饰器decorator包裹了函数,并修改了函数的行为

下面是另一个例子,这个例子中根据系统时间决定是否执行say_whee函数。

from datetime import datetime

def not_during_the_night(func):
    def wrapper():
        if 7 <= datetime.now().hour < 22:
            func()
        else:
            pass  # Hush, the neighbors are asleep
    return wrapper

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

say_whee = not_during_the_night(say_whee)

在夜间调用say_whee,则有如下响应

>>> say_whee()
>>>

syntactic sugar语法糖

上面提供的装饰器形式略复杂,python提供了一种简单的形式,即使用@符号,又称pi语法(pi syntax)。

def my_decorator(func):
    def wrapper():
        print('do something before conducting the function')
        func()
        print('do something after conducting the function')
    return wrapper

@my_decorator
def say_whee():
    return 'whee'

在这里用@my_decorator代替say_whee = my_decorator(say_whee)命令。

此外,装饰器的复用可类比类的复用,即,将装饰器函数写在一个文件中,使用时通过import指令调用即可。在后文中装饰器函数都假定写在decorator.py文件中,调用时使用诸如from decorator import xxx的指令。

装饰器修饰带参数的函数

如果在装饰器函数中的func函数没有任何传入参数的标识,则被装饰器修饰和调用的函数也不能传入参数。为了传参,可在装饰器的包裹函数中加入*args, **kwargs指令,能够保证函数的参数足够多。

# decorator.py
def do_twice(func):
    def wrapper(*args, **kwargs):
         func(*args, **kwargs)
         func(*args, **kwargs)
    return wrapper
def do_twice_no_args(func):
    def wrapper():
         func()
         func()
    return wrapper
from decorator import do_twice
@do_twice
def say_whee(name):
    return 'whee '+str(name)
@do_twice_no_args
def say_something(sth):
    return 'say '+str(sth)
>>> say_something('sth')
Traceback (most recent call last):
  File "", line 1, in 
TypeError: wrapper() takes 0 positional arguments but 1 was given
>>> say_whee('jeff')
whee jeff

被装饰函数的返回值

注意到前面提到的装饰器函数中,包裹函数内部都没有返回func函数的返回值。因此如果调用do_twice装饰器会返回如下结果

from decorators import do_twice

@do_twice
def return_greeting(name):
    print("Creating greeting")
    return f"Hi {name}"
>>> a = return_greeting('jeff')
Creating greeting
Creating greeting
>>> print(a)
None

在装饰器函数的定义过程中,确保wrapper函数返回的是被包裹函数的返回值。如下

def do_twice(func):
    def wrapper(*args, **kwargs):
         func(*args, **kwargs)
         return func(*args, **kwargs)
    return wrapper
>>> return_greeting('jeff')
Creating greeting
Creating greeting
Hi jeff

装饰器中的对象身份和@functools.wraps(func)

python的对象有一种introspection能力,introspection is an ability of an object to know about its own attributes at runtime. 比如

>>> print.__name__
'print'
>>> help(print)
Help on built-in function print in module builtins:
...
>>> say_whee.__name__
'wrapper'
>>> help(say_whee)
Help on function wrapper in module __main__:

wrapper()

上面例子中可以看出,当say_whee函数被装饰之后,对其自身的身份产生的confused,调用.__name__返回的是装饰器中的包裹函数wrapper。为解决这个问题,需要在装饰器中加入@functools.wraps(func)命令,该命令可使被装饰的函数保留其原始信息,而免于被装饰器的wrapper函数覆盖。

import functools

def do_twice(func):
    @functools.wraps(func)
    def wrapper_do_twice(*args, **kwargs):
        func(*args, **kwargs)
        return func(*args, **kwargs)
    return wrapper_do_twice
>>> say_whee

>>> say_whee.__name__
'say_whee'
>>> help(say_whee)
Help on function say_whee in module whee:
say_whee()

Decorator常见应用

(2022.02.11 Fri wellness day)
根据前面的内容,一个典型装饰器函数的结构如下所示,其他装饰器都可以此为模板生成

import functools

def decorator(func):
    @functools.wraps(func)
    def wrapper_decorator(*args, **kwargs):
        # Do something prior to func function
        value = func(*args, **kwargs)
        # Do something after func function
        return value
    return wrapper_decorator

计时函数

import functools
import time

def timer(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        starting_time = time.perf_counter()
        result = func(*args, **kwargs)
        ending_time = time.perf_counter()
        print('time consumption in seconds: ', ending_time - starting_time)
        return result
    return wrapper

@timer
def func(n):
    for _ in range(n):
        sum([i**2 for i in range(10000)])

调用

>>> func(100)
time consumption in seconds:  0.2192373275756836

延时

import functools
import time
def slowdown(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        time.sleep(1)
        value = func(*args, **kwargs)
        return value
    return wrapper

@slowdown
def countdown(number):
    if number < 1:
        print('liftoff')
    else:
        print(number)
        countdown(number-1)

倒计时

>>> countdown(3)
3
2
1
liftoff

登录查询

在web framework,如Flask,Django中,需要经常检验用户是否登录,可用装饰器来实现。

from flask import Flask, g, request, redirect, url_for
import functools
app = Flask(__name__)

def login_required(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        if g.user is None:
            return redirect(url_for('login', next=request.url))
        return func(*args, **kwargs)
    return wrapper

@app.route('/secret')
@login_required
def secret():
   ...

上面提供的是装饰器做login验证的一个案例,在实际应用中,使用Flask-login extension用以提升安全性。

注册插件Registering Plugin和加入信息

装饰器不仅可以包裹一个被装饰函数,也可以仅仅是登记或注册函数的信息而不对函数行为做任何改变。

import random
PLUGINS = dict()

def register(func):
    # register a function as a plugin
    PLUGINS[func.__name__] = func
    return func

@register
def say_hi():
    return "hi"

@register
def be_awesome(name):
    return f"Yo {name}, together we are the awesomest!"

def randomly_greet(name):
    greeter, greeter_func = random.choice(list(PLUGINS.items()))
    print(f"Using {greeter!r}")
    return greeter_func(name)

这个案例中,装饰器函数中仅仅保存了对被装饰函数的引用,没有对被装饰函数做任何修改。也因此,不需要在装饰器的内部函数中使用@functools.wraps(func)指令。

(2022.02.24 Thur 补充)
通过类似的方式,可以实现为被装饰函数加单位的功能。如下

def set_unit(unit):
    # register a unit on a function
    def decorator_set_unit(func):
        func.unit = unit
        return func
    return decorator_set_unit

调用时

>>> @set_unit("cm^3")
... def volume(radius, height):
...     return math.pi * radius**2 * height
... 
>>> volume(3,4)
113.09733552923255
>>> volume.unit
'cm^3'

Python中同样提供了库pint用于转换不同的单位。比如下面的例子。

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

>>> vol.to("cubic inches")

>>> vol.to("gallons").m  # Magnitude
0.0373464440537444
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

单位转换无压力

>>> bolt = average_speed(100, 9.58)
>>> bolt

>>> bolt.to("km per hour")

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

debug

装饰器可用以debug

import functools
def debug(func):
    """Print the function signature and return value"""
    @functools.wraps(func)
    def wrapper_debug(*args, **kwargs):
        args_repr = [repr(a) for a in args]                      # 1
        kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]  # 2
        signature = ", ".join(args_repr + kwargs_repr)           # 3
        print(f"Calling {func.__name__}({signature})")
        value = func(*args, **kwargs)
        print(f"{func.__name__!r} returned {value!r}")           # 4
        return value
    return wrapper_debug

说明:

  • signature是对所有变量的字符表达做join操作得到的结果
  • repr()函数用于将传入的对象转换成string格式,包括但不限于int, list, dict等数据结构
  • !r是python字符串表达表达中的转译字符,表示在字符串中引用的变量需要转换成repo(x)的形式,如
>>> a = [1]
>>> f"""{a!r}"""
'[1]'
>>> 'hello, {!r}'.format(a)
'hello, [1]'

另外,x!s代表str(x),x!a代表ascii(x)。
调用如下

@debug
def make_greeting(name, age=None):
    if age is None:
        return f"Howdy {name}!"
    else:
        return f"Whoa {name}! {age} already, you are growing up!"
>>> make_greeting('dave', 18)
Calling make_greeting('dave', 18)
'make_greeting' returned 'Whoa dave! 18 already, you are growing up!'
'Whoa dave! 18 already, you are growing up!'

Decorator高级使用

作用于类的装饰器

装饰器作用于类,可以通过两种方式,分别是对类中方法做装饰对类做装饰

对类中方法做装饰,有自定义装饰器装饰和用类内部的装饰器装饰两种。
(2022.02.22 Tues)
类装饰器包括@classmethod, @staticmethod, @property等。更多细节,参考文章Python类装饰器。
可使用自定义装饰器装饰类中的方法,使用方式如同装饰普通函数。

class demoClass:
    @debug
    def __init__(self):
        pass
    @timer
    def time_consumption(self, n):
        for _ in range(n):
            sum([i**2 for i in range(n)])

装饰器也可装饰一个类。Python 3.7版本中引入了dataclasses包。

from dataclasses import dataclass
@dataclass
class PlayingCard:
    pass

如上语句相当于PlayingCard = dataclass(PlayingCard)
同样可以自定义装饰器函数用于装饰类。与前面的装饰器案例不同的是,修饰类的装饰器,其输入函数是类而非一个函数。下面是用@timer修饰类的例子

@timer
class demoClass: 
    def __init__(self):
        pass 
    def time_consumption(self, n):
        for _ in range(n):
            sum([i**2 for i in range(n)])

思考这个timer会返回什么结果?根绝timer函数定义,该装饰器仅在类实例化时工作,而调用类方法时不返回结果。
装饰器定义

import functools
import time

def timer(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        starting_time = time.perf_counter()
        result = func(*args, **kwargs) # 注意这条指令,仅仅是实例化时发生作用
        ending_time = time.perf_counter()
        print('time consumption in seconds: ', ending_time - starting_time)
        return result
    return wrapper

来看装饰器的结果

>>> a = demoClass()
time consumption in seconds:  0.0009906610002872185
>>> a.time_consumption(5)
>>> 

多个装饰器修饰同一个函数 Nesting decorators

(2022.02.22 Tues)
多个装饰器修饰同一个函数,按照从上到下的顺序(stack)套用。来看一个案例

import functools
def p1(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print('1: begin')
        result = func(*args, **kwargs) 
        print('1: end')
        return result
    return wrapper
import functools
def p2(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print('2: begin')
        result = func(*args, **kwargs)  
        print('2: end')
        return result
    return wrapper

调用

@p1
@p2
def p3():
    print('3')
>>> p3()
1: begin
2: begin
3
2: end
1: end

以上的装饰器用法,相当于p3 = p1(p2(p3)),因此有案例中的结果。

装饰器的参数

(2022.02.22 Tues)
装饰器可以加入参数,该参数需要在装饰器的定义函数中作为传递参数出现。比如@do_twice这个装饰器可以重新定义为@repeat装饰器,并传入repeat次数。在前面举例的装饰器基础之上再加一层函数。

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

调用如下

@repeat(3)
def p3():
    print('100')
>>> p3()
100
100
100

这里我们注意到,一旦装饰器可以传入参数,则装饰器的定义中需要加一个extra outer function.

装饰器的可选参数

(2022.02.23 Wed)
Since the function to decorate is only passed in directly if the decorator is called without arguments, the function must be an optional argument. This means that the decorator arguments must all be specified by keyword. You can enforce this with the special * syntax.
下面是一个模板。

def name(_func=None, *, kw1=val1, kw2=val2):
    def decorator_name(func):
        ... # create and return a wrapper
    
    if _func is None:
        return decorator_name
    else:
        return decorator_name(_func)

将该模板应用于repeat函数。待反思。

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

装饰器记录状态stateful decorator

(2022.02.23 Wed)
前面的装饰器无法记录状态,这里我们探索在装饰器中加入状态,记录之前的record。后面的内容会介绍如何用类保存状态。

import functools
def call_cntr(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        wrapper.cntr += 1
        print('call number: ', wrapper.cntr)
        value = func(*args, **kwargs)
        return value
    wrapper.cntr = 0
    return wrapper

调用

>>> @call_cntr
... def p1():
...     print('100')
... 
>>> p1()
call number:  1
100
>>> p1()
call number:  2
100
>>> @call_cntr
... def p2():
...     print('100')
... 
>>> p2()
call number:  1
100
>>> p1()
call number:  4
100

类作为装饰器

(2022.02.23 Wed)
Python的语法糖@my_decorator,等效于func = my_decorator(func),其中的my_decorator是预先定义的函数。如果my_decorator是一个类,则输入的函数func相当于是类的初始化方法__init__的一个输入值。不仅如此,作为装饰器的类还可以被调用(callable),以实现装饰器的功能。
上节的计数器功能,可以通过如下方法实现。其中的__call__这个magic method在类的实例每一次被调用时执行。

class Cntr:
    def __init__(self, start=0):
        self.cntr = start
    def __call__(self):
        self.cntr += 1
        return f'''Count is {self.cntr}'''

实例化

>>> a = Cntr()
>>> a()
'Count is 1'
>>> a()
'Count is 2'

下面是类做装饰器的典型实现

import functools
class CountCalls:
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.call_num = 0
    def __call__(self, *args, **kwargs):
        self.call_num += 1
        print('call num: ', self.call_num)
        return self.func(*args, **kwargs)

调用

>>> @CountCalls
    def p1():
        print('100')

>>> p1()
call num:  1
100
>>> p1()
call num:  2
100

Decorator的高级应用

延时的更多用法

(2022.02.23 Wed)
前面的案例中,装饰器的延时只有一秒钟。考虑到我们可以在装饰器中传入参数,延时也因此可以通过参数控制。

import functools, time
def slow_down(_func=None, *, gap=1):
    def decorator_slow_down(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            time.sleep(gap)
            value = func(*args, **kwargs)
            return value
        return wrapper
    if _func is None:
        return decorator_slow_down
    else:
        return decorator_slow_down(_func)

调用

>>> @slow_down(gap=1.9)
    def p2():
        print('2')
>>> p2()
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!

创建单例singleton

(2022.02.24 Thur)
Singleton单例模式,即只有一个实例化的类。Python的单例模式常见的有NoneTrueFalse。Python中的其他单例模式的案例还有内置包的.pyc文件,该文件在类第一次被调用时生成,之后的每次调用包都是直接调用该文件,实现单例模式。None和其他变量比较时,需要用到is关键字。在Python内存管理这个文章中介绍过,is关键字和==的差别在于,前者用于比较关键字左右两端的变量是否指向同一对象,而后者用于比较两端变量指向对象的实体/值是否相同。考虑到None关键字是单例,只能用is关键字。

下面的@singleton装饰器在第一次使用时生成类的实例并保存为一个属性,之后每次生成对象都调用该属性。从而实现了单例模式。注意,单例模式也有其他的生成方式,而且注意其线程的使用。

import functools
def singleton(cls):
    # make a class a singleton class
    @functools.wraps(cls)
    def singleton_wrapper(*args, **kwargs):
        if not singleton_wrapper.instance:
            singleton_wrapper.instance = cls(*args, **kwargs)
        return singleton_wrapper.instance
    singleton_wrapper.instance = None # 这句是干嘛的
    return singleton_wrapper

调用

>>> @singleton
... class onec:
...     pass
... 
>>> m = onec()
>>> n = onec()
>>> id(m)
4548827264
>>> id(n)
4548827264
>>> m is n
True
>>> m == n
True

缓存返回值caching return value

(2022.02.24 Thur)
装饰器可以用来缓存计算过程中的值,比如recursive中的caching和memoization。我们从Fibonacci数列的计算入手。用@call_cntr装饰器计算Fibonacci数列计算中被调用的次数。

@call_cntr
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

调用查看调用次数。仅仅计算到10,就被调用192次。

>>> fibonacci(10)
...
call number:  192
55

面对这种情况的解决方案是使用for循环和查询表(loopup table)。同样也可使用缓存解决该问题。

def cache(func):
    # keep a cahce for previous function calls
    @functools.wraps(func)
    def cache_wrapper(*args, **kwargs):
        cache_key = args + tuple(kwargs.items())
        if cache_key not in cache_wrapper.cache:
            cache_wrapper.cache[cache_key] = func(*args, **kwargs)
        return cache_wrapper.cache[cache_key]
    cache_wrapper.cache = dict()
    return cache_wrapper

调用查看结果

>>> @cache
... @call_cntr
... def fibonacci(num):
...     if num < 2:
...         return num
...     return fibonacci(num - 1) + fibonacci(num - 2)
... 
>>> fibonacci(10)
call number:  1
...
call number:  11
55
>>> fibonacci(9)
34

上面结果看到,计算到10,仅调用11次,而后计算9,不需要调用,因数据都缓存到装饰器中。

Python标准库提供了替代上述装饰器的功能,即Least Recently Used(LRU)缓存,@functools.lru_cache,推荐使用。参数中的maxsize表示缓存的最大个数。

@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)

调用

>>> fibonacci(10)
Calculating fibonacci(10)
...
Calculating fibonacci(0)
55
>>> fibonacci.cache_info()
CacheInfo(hits=8, misses=11, maxsize=4, currsize=4)

验证JSON validating JSON

(2022.02.24 Thur)
我们考虑Flask的应用。在这个案例中,检测用户从页面POST的数据,其中如果没有student_id这个字段,则返回400错误。如果在处理不同的URL时都需要检测该字段,则可使用装饰器实现代码复用。

@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!"

动手写一个装饰器,用以实现判断输入是否有student_id字段。

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

def validate_json(*expected_json):
    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:  
                if expected_arg not in json_object:
                    abort(400)
            return func(*args, **kwargs)
        return wrapper_validate_json
    return decorator_validate_json

调用并代替原函数中的功能

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

Reference

1 realpython com primer-on-python-decorators (https冒号//realpython点com/primer-on-python-decorators/)
2 知乎麦芽卷卷

你可能感兴趣的:(Python装饰器, since 2022-02-10)