入门&进阶python装饰器

前言

学了这么久的python,除了掌握if,else外是不是感觉啥都不会?不急,本系列来教大家如何在代码上更上一个层次,let us see 一下Python装饰器,这是一个非常好用的特性,运用装饰器会让代码看起来更加优雅,运行效率也将大大提升。

装饰器入门

概念:python装饰器就是用于拓展原来函数功能的一种函数,是可调用的对象,可以像常规的可调用对象那样调用,这个函数的特殊之处在于它的参数是一个函数,返回值也是一个函数,使用装饰器的好处就是在不用更改原函数的代码前提下给函数增加新的功能。概括的讲,装饰器的作用就是为已经存在的函数或对象添加额外的功能。

使用场景:它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。

装饰器简单的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def decorate(func):
def wrapper():
print('被装饰的函数是:{0}'.format(func.__name__))
return func()
return wrapper

@decorate
def hello():
print('hello!')

hello()

输出结果:
被装饰的函数是:hello
hello!

分析装饰器例子

装饰器的存在是为了适用两个场景,一个是增强被装饰函数的行为,另一个是代码重用。

比如在上面的例子中,原本的hello() 函数只是输出”hello!”,但在使用装饰器decorate()后,它的功能多了一项:输出被装饰函数的名称。这就是一个简单的装饰器,实现了增强被装饰函数的行为

一个良好的装饰器必须要遵守两个原则:

1.不能修改被装饰函数的代码

2.不能修改被装饰函数的调用方式

这里并不难以理解,在现在的生产环境中,很多代码是不能轻易的改写,因为这样有可能发送意想不到的影响。还有一点就是我们在看大神的代码,我们根本不懂如何改写。同时你也不能修改调用方式,因为你并不知道有在一个项目中,有多少处应用了此函数。

装饰器理解基础

想要更好的理解装饰器,那下面的内容需要你先有所认知。

函数名可以赋值给变量

我们来看下这个例子:

1
2
3
4
5
6
7
8
9
10
11
def func(name):
print('我喜欢吃{}!'.format(name))

func('牛肉火锅')
y = func
y('椰子鸡')


输出结果:
我喜欢吃牛肉火锅!
我喜欢吃椰子鸡!

在代码中我们首先定义了函数 func,并调用了 func 函数,并且还把 func 函数赋值给 y。y = func 表明了:函数名可以赋值给变量,并且不影响调用

高阶函数

高阶函数满足如下的两个条件中的任意一个:1.可以接收函数名作为实参;2.返回值中可以包含函数名

  • 在 Python 标准库中的 map 和 filter 等函数就是高阶函数。
1
2
3
4
5
6
7
8
9
10
l = [1, 2, 3]
r = map(lambda x: x, l)
for i in r:
print('当前数字是:', i)

输出结果:

当前数字是: 1
当前数字是: 2
当前数字是: 3
  • 自定义一个能返回函数的函数,也是高阶函数
1
2
3
4
5
6
7
8
9
10
11
12
l = [1, 2, 3]
def func(l):
return map(lambda x: x *2, l)
a = func(l)
for i in a:
print('当前数字是:', i)

输出结果:

当前数字是: 2
当前数字是: 4
当前数字是: 6

实现一个类似的装饰器

现在你已经知道了「函数名赋值」「高阶函数」,有了这两个基础,我们就可以尝试实现一个类似的装饰器。

1
2
3
4
5
6
7
8
9
def hello(func):
print('hello!')
return func

def who():
print('world!')

temp = hello(who)
temp()

输出结果:

1
2
hello!
world!

在这个例子中我们定义了一个 hello() 函数,hello() 接收一个函数名然后直接返回该函数。这样我们实现了不修改原函数 who(),并且添加了一个新功能的需求。但是这里有个缺陷就是函数的调用方式改变了,不是原本的 who(),而是 temp()。

要解决这个问题很简单,相信 a = a*3 这样的表达式大家都见过,那么上述代码中的 temp = hello(who) 同样可以修改为 who = hello(who),这样我们就完美的解决了问题:既添加新功能又没有修改原函数和其调用方式。修改后的代码如下:

1
2
3
4
5
6
7
8
9
def hello(func):
print('hello!')
return func

def who():
print('world!')

who = hello(who)
who()

但这样的代码却有个不便之处,即每次使用这样的装饰器,我们都要写类似 who = hello(who) 的代码。在 python 中为了简化这种情况,提供了一个语法糖 @ ,在每个被装饰的函数上方使用这个语法糖就可以省掉这一句代码 who = hello(who),最后的代码如下:

1
2
3
4
5
6
7
8
9
def hello(func):
print('hello!')
return func

@hello
def who():
print('world!')

who()

这样我们就弄清楚了装饰器的工作原理:

1.写一个高阶函数,即参数是函数,返回的也是函数。

2.在利用语法糖@,简化赋值操作。

装饰带参数的函数

在第一个简单例子中的被装饰函数hello()是不带参数的,如果被装饰函数需要传入参数,那需要指定装饰器也要返回和原函数一样的参数,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def decorate(func):
def wrapper(who): #指定跟原函数一样的参数
print('被装饰的函数是:{0}'.format(func.__name__))
return func(who)
return wrapper

@decorate
def hello(who):
print('hello!{0}'.format(who))

hello('张三')

输出结果:
被装饰的函数是:hello
hello!张三

看样子问题好像解决了,但是并不能预知其它被装饰函数带了什么参数,这样装饰器就被局限了。还好python提供了可变参数 *args关键字参数**kwargs ,这样装饰器就可以用于任意目标函数了。修改如下:

1
2
3
4
5
6
7
8
9
def decorate(func):
def wrapper(*args, **kwargs): #指定无敌的可变参数
print('被装饰的函数是:{0}'.format(func.__name__))
return func(*args, **kwargs)
return wrapper

@decorate
def hello(who):
print('hello!{0}'.format(who))

装饰器高级进阶

带参数的装饰器

上文提到,装饰器的参数要跟被装饰函数的一致,为了增强装饰器的使用性,我们还可以给装饰器带上参数。比如下面这个例子:

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
def logging(level):     #装饰器带参数
def wrapper(func):
def inner_wrapper(*args, **kwargs): #被装饰函数带参数
print("[{level}]: enter function {func}()".format(
level=level,
func=func.__name__))
return func(*args, **kwargs)
return inner_wrapper
return wrapper

@logging(level='INFO')
def test(something):
print("{}!".format(something))

# 如果没有使用@语法,等同于
# test = logging(level='INFO')(test)

@logging(level='DEBUG')
def name(something):
print("{}...".format(something))

if __name__ == '__main__':
test('hello world')
name("say bye")

输出结果:
[INFO]: enter function test()
hello world!
[DEBUG]: enter function name()
say bye...

当带参数的装饰器被打在某个函数上时,比如@logging(level=’DEBUG’),它其实是一个函数,会马上被执行,只要这个它返回的结果是一个装饰器时,那就是符合规则的。

类装饰器

除了用函数来实现装饰器,我们还可以用类来实现装饰器,只不过要比函数复杂一点。熟悉python的朋友知道,类是需要初始化后才能调用类实例,那如果想让类实例可以成为像函数一样能被直接调用的对象,我们需要重载__call__()方法。比如:

1
2
3
4
5
6
7
8
9
10
11
12
class logging(object):
def __init__(self, func): #类的构造函数,传入函数func
self.func = func

def __call__(self, *args, **kwargs): #类实例,相当于装饰器
print( "[DEBUG]: enter function {func}()".format(
func=self.func.__name__))
return self.func(*args, **kwargs) #返回一个函数对象

@logging
def test(something):
print("test func {}!".format(something))

带参数的类装饰器

如果需要通过类形式实现带参数的装饰器,在构造函数里接受的就不是一个函数,而是传入的参数。通过类把这些参数保存起来。然后在重载call()方法是就需要接受一个函数并返回一个函数。比如:

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

def __call__(self, func): # 接受函数
def wrapper(*args, **kwargs): # 装饰器实现函数
print("[{level}]: enter function {func}()".format(
level=self.level,
func=func.__name__))

return func(*args, **kwargs)

return wrapper # 返回函数

@logging(level='INFO')
def test(something):
print("test func {}!".format(something))

内置的装饰器

python本身有内置的装饰器,常用的有@property,@staticmethod,@classmethod。

  • @property

使调用类中的方法像引用类中的字段属性一样,经过@property装饰过的函数返回的不再是一个函数,而是一个property对象。

  • @staticmethod

将类中的方法装饰为静态方法,即类不需要创建实例的情况下,可以通过类名直接引用。到达将函数功能与实例解绑的效果。

1
2
3
4
5
6
7
8
9
10
11
class TestClass:
name = "test"

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

@staticmethod
def fun(x, y): #无需传入self,fun()作为单独的静态方法
return x + y

TestClass.fun(2, 3) #无需实例化类即可调用

  • @classmethod

类定义时,除了new方法外,其他定义的方法在调用时第一个参数传入的必须是一个实例,使用classmethod装饰器装饰后,方法的第一个参数是类对象;调用类方法不需要创建类的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class TestClass:
name = "test"

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

@classmethod
def func(lei, f_name): #无需传入self,func()作为单独的静态方法,第一个参数lei表示传入的是TestClass类
print(lei.name) #调用类的属性或方法
print(f_name)

TestClass.func('hello') #无需实例化类即可调用

输出:
test
hello

使用装饰的注意事项

  • 尽量不要在装饰器函数外增加逻辑功能,这样有可能会控制不住装饰器的执行顺序。
  • 被装饰器装饰过的原函数,实际上已经对原函数做了改造,因为被装饰器装饰过的函数调用方式实际上是func = logging(func),所以当想获取原函数func的其它属性时已经被logging()装饰器覆盖掉,解决方法是引入wraps。比如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from functools import wraps

def logging(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__))
return func(*args, **kwargs)
return wrapper

@logging
def say(something):
print("say {}!".format(something))

print say.__name__ # say
print say.__doc__ # say something
  • 当函数已经被@staticmethod 或者 @classmethod装饰时,再想用其它装饰器,应该要置放在内置函数之前,也就是
1
2
3
4
@staticmethod
@logging
def check(name):
pass

优化装饰器

  • decorator.py

这是一个非常简单的装饰器加强包,简单粗暴如下:

1
2
3
4
5
6
7
8
9
10
from decorator import decorator

@decorator # 直接使用自带的@decorator即可完成装饰器功能,简化定义代码量
def logging(func, *args, **kwargs):
print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__)
return func(*args, **kwargs)

@logging
def func(name):
pass

注:decorator.py实现的装饰器能完整保留原函数的name,doc和args,唯一有问题的就是inspect.getsource(func)返回的还是装饰器的源代码,你需要改成 inspect.getsource(func.__wrapped__)

  • wrapt

这是一个功能完善的python包,完美解决了上述decorator.py所出现的函数签名inspect的问题,如下:

1
2
3
4
5
6
7
8
9
10
import wrapt

@wrapt.decorator
def logging(func, instance, args, kwargs): # instance是必须传入的参数,func是传入的函数
print "[DEBUG]: enter {}()".format(wrapped.__name__)
return func(*args, **kwargs)

@logging
def fun(name):
pass

注:关于wrapt的使用可详看,http://wrapt.readthedocs.io/en/latest/quick-start.html

总结

日常开发中可善用装饰器,行为良好的装饰器可以重用,以减少代码量,增加代码可读性,让我们都做一个优雅的python人。

0%