(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),这意味着函数可以被传递,并作为参数来使用,如同string
,int
,list
等对象。比如
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_hello
给greet_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_child
或second_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的单例模式常见的有None
,True
和False
。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 知乎麦芽卷卷