Python装饰器的理解和应用

装饰器的作用

  • 打印日志: @log
  • 检测性能:@performance
  • 数据库事务: @transaction
  • URL路由:@post(‘/register’)

装饰器的目的:

  • 使代码可读性更高,感觉高大上;
  • 代码结构更加清晰,代码冗余度更低;

无参数decorator

Python的 decorator 本质上就是一个高阶函数,它接收一个函数作为参数,然后,返回一个新函数。

使用 decorator 用Python提供的 @ 语法,这样可以避免手动编写f = decorate(f) 这样的代码。

考察一个@log的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def testlog(func):
def inner(arg):
print("print log:" + func.__name__)
func(arg)
return inner

# 不使用@
def test(x):
print("this is test function:",x)

test = testlog(test)
test("11")

# 使用@
@testlog
def test(x):
print("this is test function:",x)

test("11")

# print log:test
# this is test function:11

例子:阶乘

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from functools import reduce

def testlog(func):
def inner(*args,**kwargs):
# 要让 @testlog 自适应任何参数定义的函数,可以利用Python的 *args 和 **kw,保证任意个数的参数总是能正常调用
print("print log:" + func.__name__)
func(*args,**kwargs)
return inner

@testlog
def factorial(x, y):
print("this is factorial function:")
res = reduce(lambda x, y: x * y, range(x, y))
print(res)
return res

factorial(1, 11)
# print log:factorial
# this is factorial function:
# 3628800

例子:写一个@performance,作为一个函数计时器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import time

def performance(func):
def inner(*args, **kwargs):
time1 = time.time()
res = func(*args, **kwargs)
time2 = time.time()
print(f"call %s%s%s in %fs" % (func.__name__, args,kwargs, time2 - time1))
return res
return inner

@performance
def factorial(x, y):
return reduce(lambda x, y: x * y, range(x, y))

print(factorial(1, 11))
# call factorial(1, 11){} in 0.000999s
# 3628800

有参数decorator

在之前没有带参数的装饰器上添加参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from functools import reduce
import time

def performance(unit):
def decorator_(func):
def wrap(*args, **kwargs):
time1 = time.time()
res = func(*args, **kwargs)
time.sleep(1)
time2 = time.time()
t = 1000 * (time2 - time1) if unit == 'ms' else (time2 - time1)
print(f"call %s in %s %s"%(func.__name__, t, unit))
return res
return wrap
return decorator_

@performance('ms')
def factorial_(n):
return reduce(lambda x,y: x*y, range(1, n+1))

print(factorial_(6))
# call factorial_ in 1000.087261199951 ms
# 720

装饰器的优化

@decorator 可以动态实现函数功能的增加,但是,经过@decorator“改造”后的函数,和原函数相比,除了功能多一点外,还有其它不同的地方。

在没有decorator的情况下,打印函数名:

1
2
3
def f1(x):
pass
print(f1.__name__) # f1

有decorator的情况下,再打印函数名:

1
2
3
4
5
6
7
8
def log(func):
def wrapper(*args, **kw):
return func(*args, **kw)
return wrapper
@log
def f2(x):
pass
print(f2.__name__) # wrapper

由于decorator返回的新函数函数名已经不是’f2’,而是@log内部定义的’wrapper’。这对于那些依赖函数名的代码就会失效。decorator还改变了函数的 __doc__ 等其它属性。如果要让调用者看不出一个函数经过了@decorator的“改造”,就需要把原函数的一些属性复制到新函数中:

1
2
3
4
5
6
7
8
9
10
def log(func):
def wrapper(*args, **kw):
return func(*args, **kw)
wrapper.__name__ = func.__name__
wrapper.__doc__ = func.__doc__
return wrapper
@log
def f2(x):
pass
print(f2.__name__) # f2

这样写decorator很不方便,也很难把原函数的所有必要属性都一个一个复制到新函数上,所以Python内置的 functools 可以用来自动化完成这个“复制”的任务:

1
2
3
4
5
6
7
8
9
10
import functools
def log(func):
@functools.wraps(func)
def wrapper(*args, **kw):
return func(*args, **kw)
return wrapper
@log
def f2(x):
pass
print(f2.__name__) # f2
1
2
3
4
5
6
7
8
9
10
11
12
13
# 带参数的装饰器
# 注意@functools.wraps应该作用在返回的新函数上。
def performance(prefix):
def log(func):
@functools.wraps(func)
def wrapper(*args, **kw):
return func(*args, **kw)
return wrapper
return log
@performance("debug")
def f2(x):
pass
print(f2.__name__) # f2

类装饰器(不带参数)

基于类装饰器的实现,必须实现 __call____init__两个内置函数。
__init__ :接收被装饰函数
__call__ :实现装饰逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class logger(object):
def __init__(self, func):
self.func = func

def __call__(self, *args, **kwargs):
print(f"logger:{self.func.__name__} in decorator")
return self.func(*args, **kwargs)

@logger
def test(para):
print(f"test func({para})")

test("123")
# logger:test in decorator
# test func(123)

类装饰器(带参数)

若还需要打印DEBUG WARNING等级别的日志。这就需要给类装饰器传入参数,给这个函数指定级别了。

带参数和不带参数的类装饰器有很大的不同。

__init__ :不再接收被装饰函数,而是接收传入参数。
__call__ :接收被装饰函数,实现装饰逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class logger(object):
def __init__(self, level='info'):
self.level = level

def __call__(self, func):
def wrapper(*args, **kwargs):
print(f"logger[{self.level}]:{func.__name__} in decorator")
func(*args, **kwargs)
return wrapper

@logger(level='warning')
def test_funct(para):
print(f'test test_funct({para})')

test_funct("321")
# logger[warning]:test_funct in decorator
# test test_funct(321)

偏函数

当一个函数有很多参数时,调用者就需要提供多个参数。如果减少参数个数,就可以简化写代码的负担。

比如,int()函数可以把字符串转换为整数,当仅传入字符串时,int()函数默认按十进制转换:

1
2
>>> int('12345')
12345

但int()函数还提供额外的base参数,默认值为10。如果传入base参数,就可以做 N 进制的转换:

1
2
3
4
>>> int('12345', base=8)
5349
>>> int('12345', 16)
74565

假设要转换大量的二进制字符串,每次都传入int(x, base=2)非常麻烦,于是,我们想到,可以定义一个int2()的函数,默认把base=2传进去:

1
2
def int2(x, base=2):
return int(x, base)

这样,我们转换二进制就非常方便了:

1
2
3
4
>>> int2('1000000')
64
>>> int2('1010101')
85

functools.partial 就是帮助我们创建一个偏函数的,不需要我们自己定义int2(),可以直接使用下面的代码创建一个新的函数int2:

1
2
3
4
5
6
>>> import functools
>>> int2 = functools.partial(int, base=2)
>>> int2('1000000')
64
>>> int2("11",base=2) # 临时需要可以更改
3

所以,functools.partial 可以把一个多参数的函数变成一个参数少的新函数,少的参数需要在创建时指定默认值,这样,新函数调用的难度就降低了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from functools import partial

# 示例 1: 文件处理中的应用
# 基础函数
def read_file(filename, mode='r', encoding='utf-8', errors='strict'):
with open(filename, mode=mode, encoding=encoding, errors=errors) as f:
return f.read()

# 创建一个专门用于处理日志文件的偏函数,固定编码和错误处理方式
read_log = partial(read_file, encoding='utf-8', errors='ignore')

# 使用示例
# log_content = read_log('app.log') # 只需要提供文件名即可


# 示例 2: 数值处理中的应用
def round_number(number, ndigits=0, base=10):
"""按指定进位基数四舍五入"""
return round(number / base) * base

# 创建一个专门用于处理十位数四舍五入的偏函数
round_to_tens = partial(round_number, base=10)
round_to_hundreds = partial(round_number, base=100)

# 使用示例
print(round_to_tens(126)) # 输出: 130
print(round_to_hundreds(126)) # 输出: 100

偏函数与类实现装饰器

大多数装饰器都是基于函数和闭包实现的,但这并非构造装饰器的唯一方式。

Python 对某个对象是否能通过装饰器( @decorator)形式使用只有一个要求:decorator 必须是一个可被调用(callable)的对象

对于callable 对象,最熟悉的就是函数了;

除函数之外,类也可以是 callable 对象,只要实现了__call__ 函数(上面几个例子)。

还有容易被人忽略的偏函数其实也是 callable 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import time
import functools

class DelayFunc:
def __init__(self, duration, func):
self.duration = duration
self.func = func

def __call__(self, *args, **kwargs):
print(f'Wait for {self.duration} seconds...')
time.sleep(self.duration)
return self.func(*args, **kwargs)

def eager_call(self, *args, **kwargs):
print('Call without delay')
return self.func(*args, **kwargs)

def delay(duration):
"""
装饰器:推迟某个函数的执行。
同时提供 .eager_call 方法立即执行
"""
# 此处为了避免定义额外函数,
# 直接使用 functools.partial 帮助构造 DelayFunc 实例
return functools.partial(DelayFunc, duration)

@delay(duration=2)
def add(a, b):
return a+b

print(add) # add 变成了类DelayFunc的实例
# <__main__.DelayFunc object at 0x000001EED91FA3C8>
print(add(3, 5)) # 直接调用实例,进入 __call__
# Wait for 2 seconds...
# 8
print(add.eager_call(1,2)) # 实现实例方法
# Call without delay
# 3

装饰类的装饰器

用 Python 写单例模式的时候,常用的有三种写法。其中一种,是用装饰器来实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
instances = {}

def singleton(cls):
def get_instance(*args, **kwargs):
cls_name = cls.__name__
print("cls_name:",cls_name)
if not cls_name in instances:
print(f"cle_name[{cls_name}] not in instances")
instance = cls(*args, **kwargs)
instances[cls_name] = instance
return instances[cls_name]
return get_instance

@singleton
class User:
_instance = None

def __init__(self, name):
self.name = name
print("name=",self.name)

u1 =User("usr1")
print(u1,u1.name)

u2 =User("usr2")
print(u2,u2.name)

print(u1 == u2)
# cls_name: User
# cle_name[User] not in instances
# name= usr1
# <__main__.User object at 0x000001B2D33BB2B0> usr1
# cls_name: User
# <__main__.User object at 0x000001B2D33BB2B0> usr1
# True

wraps再理解

了解完偏函数后再回看 functools.wraps() 的作用

wraps 其实是一个偏函数对象(partial),源码如下:

1
2
3
4
5
def wraps(wrapped,
assigned = WRAPPER_ASSIGNMENTS,
updated = WRAPPER_UPDATES):
return partial(update_wrapper, wrapped=wrapped,
assigned=assigned, updated=updated)

wraps其实就是调用了一个函数update_wrapper,知道原理后,我们改写上面的代码,在不使用 wraps的情况下,也可以让wrapped.__name__ 打印出 wrapped,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from functools import update_wrapper

WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',
'__annotations__')

def decorator(func):
def inner():
pass
update_wrapper(inner, func, assigned=WRAPPER_ASSIGNMENTS)
return inner

@decorator
def testfunc():
pass

print(testfunc.__name__) # testfunc

一般最常使用的就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
from functools import wraps

def decorator(func):
@wraps(func)
def inner():
pass
return inner

@decorator
def testfunc():
pass

print(testfunc.__name__) # testfunc

python内置装饰器:property

property通常存在于类中,可以将一个函数定义成一个属性,属性的值就是该函数return的内容。

初学Python时这样给实例绑定属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person(object): 
def __init__(self, name, age= None):
self.name = name
self.age = age

# instantiate
p1 = Person("person1")

# add attribute
p1.age = 25

# query attrbute
print(p1.name)

# delete attribute
del p1.age

但是这样存在问题:直接把属性暴露出去,虽然写起来很简单,但是并不能对属性的值做合法性限制。合理的写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Person(object):
def __init__(self, name, age= None):
self.name = name
self.__age = age

def set_age(self, age):
if not isinstance(age, int):
raise ValueError('input illegal: age must be int.')
if not 0 < age < 100:
raise ValueError('input illegal: age must between 0 and 150.')
self.__age = age

def get_age(self):
return self.__age

def del_age(self):
self.__age = None


# instantiate
p1 = Person("tiny")

# add attribute
p1.set_age(25)

# query attrbute
print(p1.get_age())

# delete attribute
p1.del_age()

上面的代码设计虽然可以变量的定义,但是可以发现不管是获取还是赋值(通过函数)都和我们平时想要的的不同。我们想要的是这样形式的:赋值p1.age = 25, 获取p1.age

使用@property :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Person(object):
def __init__(self, name, age=None):
self.name = name
self.__age = age

@property
def age(self):
return self.__age

@age.setter
def age(self, value):
if not isinstance(value, int):
raise ValueError('age must be int.')
if not 0 < value < 150:
raise ValueError('age must between 1 and 150.')
self.__age = value

@age.deleter
def age(self):
del self.__age


# instantiate
p1 = Person("tiny")

# modify attribute
p1.age = 25

# query attrbute
print(p1.age)

# delete attribute
del p1.age

@property装饰过的函数,会将一个函数定义成一个属性,属性的值就是该函数return的内容。同时,会将这个函数变成另外一个装饰器。就像@age.setter@age.deleter

@age.setter 使得我们可以使用p1.age = 25这样的方式直接赋值;
@age.deleter 使得我们可以使用del p1.age这样的方式来删除属性。