即使一个人,也要活得像军队一样!

Python-- Python秘史之“装饰器”

写在前头,本来不打算写这些东西的,毕竟网上一大堆,官方文档一大堆。可是有些看官和徒弟不懂一些Python知识,讲解Odoo知识时候有些吃力,只能补充一下,写一写基础的。真人、神仙、佛祖级别的就自动略过。

关于Python的一些秘史啊,我们就扒一扒。那各位看官,就请搬板凳坐一排,且听我细细道来。

一、前戏

首先,我们先说对象的事情。各位single dog要爱护自己的两个五菇凉啊!这是最重要的伴侣!
Python中就有一个对象的概念。 万物皆可盘,一切皆对象。

万物皆对象

下面就说说函数。理解Python中的函数。函数是用def定义的,本质也是一个对象。

1
2
3
4
def hello():
print('单身狗,你好!')

print(type(hello))

输出结果:

1
<class 'function'>

可以看到,是一个function类型(其实还有method类型,下回讲解…)

我们甚至可以将一个函数赋值给一个变量,比如:

1
2
3
4
5
def single_dog(name)
return '%s is single dog' % name

dog = single_dog # 注意:这里没有使用小括号,也没有传参,因为我们并不是在调用函数,只是引用函数对象
print(single_dog('小明')) # output: '小明 is single dog'

此时,如果我们删掉旧的single_dog函数,看看会发生什么!

1
2
3
del single_dog
print(single_dog()) # outputs: NameError
print(dog('小王')) # outputs: '小王 is single dog'

我们发现,即使我们删除旧的方法,但是我们之前赋值引用的函数还是正常使用。
继续…

函数中定义函数

接下来,我们来个骚操作。刚才我们只是定义了一个函数,然后被赋值给一个变量。那么我们能不能在函数中再定义一个函数呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def single_dog(name="小王"):
print("%s是个单身狗" % name)

def hobby():
return "%s喜欢看小电影" % name

def sex():
return "%s是基佬" % name

print(hobby())
print(sex())

single_dog()
# output:
# 小王是个单身狗
# 小王喜欢看小电影
# 小王是基佬

发现了什么?无论怎么调用single_dog(), hobby()和sex()是一定会执行的。这也是必然的,毕竟单身狗都好这口,各位看官您说呢?
如果,我们单独调用hobby()或者sex()呢?

1
sex()  # outputs: NameError: name 'sex' is not defined

可见,我们是不能直接调用内部函数的。
那现在我们在函数中定义另外的函数。也就是说:我们可以创建嵌套的函数。但是我们不能直接调用内部函数。那我们能否在函数中返回一个函数呢?
继续…

函数中返回一个函数

其实并不需要在一个函数里去执行另一个函数,我们也可以将其作为输出返回出来:

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
def single_dog(name="小王"):
print("%s是个单身狗" % name)

def hobby():
return "%s喜欢看小电影" % name

def sex():
return "%s是基佬" % name

if name == '小明':
return hobby
else:
return sex

dog1 = single_dog()
print(dog1)
print(dog1())

dog2 = single_dog('小明')
print(dog2)
print(dog2())
# output:
# 小王是个单身狗
# <function single_dog.<locals>.sex at 0x000001D2C7C23268>
# 小王是基佬
# 小明是个单身狗
# <function single_dog.<locals>.hobby at 0x000001D2C7C231E0>
# 小明喜欢看小电影

发现了什么?dog1指向了sex函数,dog2指向了hobby函数,并且dog1和dog2都可以加()运行。
由此可见,函数是可以返回一个函数,并且正常的被调用运行的。那么我们定义函数时候,能不能传入一个函数呢?
继续…

依然是前戏。

函数作为参数传入到函数中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def single_dog(name="小王"):
print("%s是个单身狗" % name)

def hobby():
return "%s喜欢看小电影" % name

def sex():
return "%s是基佬" % name

print(hobby())
print(sex())

def is_dog(func):
print('单身狗也是狗')
func()

is_dog(single_dog)

# output:
# 单身狗也是狗
# 小王是个单身狗
# 小王喜欢看小电影
# 小王是基佬

要想做得好,前戏得做足了。说了这么久,还没进入主题。小王说他就蹭蹭不进去,那我们进去。

正主“装饰器”

装饰器?啥是装饰器?装饰器(Decorators)是 Python 的一个重要部分。简单地说:就是在不动其他函数时,给其增加功能进行丰富的函数。有助于让我们的代码更简短,也更Pythonic。
啥?不明白?要一个场景?那行吧…

比如: 果农2块的苹果卖出去后被放入盒子里,最后变成被果农儿子花10块买回来的平安果。

要个实际场景?

场景来了: 小明是个单身狗,喜欢看小电影。在电脑某个盘里存放了一部XDYJ-6961.mp4的文件。以便于夜深人静的时候观赏。但是呢,平时小王又会用他电脑。小明害怕小王发现,就对该文件进行了修饰修饰,把文件放入目录名为人类文明的起源的文件夹下。此时呢,小明晚上仍可以欣赏。这就是做到不动原本的,给其装饰了一番。

要实战姿势?其实我们上面已经写了一个装饰器了。(闭包这些个概念,您就自己百度吧…)

第一个装饰器(函数装饰函数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def hobby(func):
def wrapfunc():
print("单身狗喜欢看小电影")
func()
return wrapfunc


def single_dog(name="小王"):
print("%s是个单身狗" % name)


single_dog()
single_dog = hobby(single_dog)
single_dog()

# output:
# 小王是个单身狗
# 单身狗喜欢看小电影
# 小王是个单身狗

这就是一个装饰器,没有改变原函数,再原函数的功能上增加了一个功能。wrapfunc就是一个包装器。那有看官就问“为啥我看到的装饰器都是@开始的呢?”
那是因为@是Python的一个语法糖。上面代码等同于

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def hobby(func):
def wraps():
print("单身狗喜欢看小电影")
func()
return wraps

@hobby
def single_dog(name="小王"):
print("%s是个单身狗" % name)

single_dog()
# output:
# 单身狗喜欢看小电影
# 小王是个单身狗

这就是Pythonic的写法,至于其他的语法糖也请自行百度。

包装器的装饰器–wraps

此时,我们的装饰器是存在一些问题的。我们的被装饰的函数名字到底变成了什么呢?

1
2
3
4
print(single_dog.__name__)

# output:
# wrapfunc

显然,这不是我们想要的。函数名被替换了包装器。它重写了我们函数的名字和注释文档(docstring)。
对于受到封装的原函数来说,装饰器能够在那个函数执行前或者执行后分别运行一些代码,使得可以再装饰器里面访问并修改原函数的参数以及返回值,以实现约束定义、调试程序、注册函数等目标。装饰器一般返回一个包装器(wrapper),而functools.wraps就是装饰包装器的装饰器。

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

def hobby(func):
@wraps(func) # 包装器的装饰器
def wrapfunc():
print("单身狗喜欢看小电影")
func()
return wrapfunc


@hobby
def single_dog(name="小王"):
print("%s是个单身狗" % name)


print(single_dog.__name__)

# output:
# single_dog

wraps保留了原函数的属性

使用场景

授权(Authorization)

装饰器能有助于检查某个人是否被授权去使用一个web应用的端点(endpoint)。它们被大量使用于Flask和Django web框架中。这里是一个例子来使用基于装饰器的授权:

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

def requires_auth(f):
@wraps(f)
def decorated(*args, **kwargs):
auth = request.authorization
if not auth or not check_auth(auth.username, auth.password):
authenticate()
return f(*args, **kwargs)
return decorated

日志(Logging)

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

def logit(func):
@wraps(func)
def with_logging(*args, **kwargs):
print(func.__name__ + " was called")
return func(*args, **kwargs)
return with_logging

@logit
def addition_func(x):
"""Do some math."""
return x + x


result = addition_func(4)
# Output: addition_func was called

带参数的装饰器

我们回到日志的例子,并创建一个包裹函数,能让我们指定一个用于输出的日志文件。

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
from functools import wraps

def logit(logfile='out.log'):
def logging_decorator(func):
@wraps(func)
def wrapped_function(*args, **kwargs):
log_string = func.__name__ + " was called"
print(log_string)
# 打开logfile,并写入内容
with open(logfile, 'a') as opened_file:
# 现在将日志打到指定的logfile
opened_file.write(log_string + '\n')
return func(*args, **kwargs)
return wrapped_function
return logging_decorator

@logit()
def myfunc1():
pass

myfunc1()
# Output: myfunc1 was called
# 现在一个叫做 out.log 的文件出现了,里面的内容就是上面的字符串

@logit(logfile='func2.log')
def myfunc2():
pass

myfunc2()
# Output: myfunc2 was called
# 现在一个叫做 func2.log 的文件出现了,里面的内容就是上面的字符串

装饰器类(类装饰函数)

现在我们有了能用于正式环境的logit装饰器,但当我们的应用的某些部分还比较脆弱时,异常也许是需要更紧急关注的事情。比方说有时你只想打日志到一个文件。而有时你想把引起你注意的问题发送到一个email,同时也保留日志,留个记录。这是一个使用继承的场景,但目前为止我们只看到过用来构建装饰器的函数。

幸运的是,类也可以用来构建装饰器。那我们现在以一个类而不是一个函数的方式,来重新构建logit。

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 wraps

class logit(object):
def __init__(self, logfile='out.log'):
self.logfile = logfile

def __call__(self, func):
@wraps(func)
def wrapped_function(*args, **kwargs):
log_string = func.__name__ + " was called"
print(log_string)
# 打开logfile并写入
with open(self.logfile, 'a') as opened_file:
# 现在将日志打到指定的文件
opened_file.write(log_string + '\n')
# 现在,发送一个通知
self.notify()
return func(*args, **kwargs)
return wrapped_function

def notify(self):
# logit只打日志,不做别的
pass

这个实现有一个附加优势,在于比嵌套函数的方式更加整洁,而且包裹一个函数还是使用跟以前一样的语法:

1
2
3
@logit()
def myfunc1():
pass

现在,我们给 logit 创建子类,来添加 email 的功能。

1
2
3
4
5
6
7
8
9
10
11
12
class email_logit(logit):
'''
一个logit的实现版本,可以在函数调用时发送email给管理员
'''
def __init__(self, email='admin@myproject.com', *args, **kwargs):
self.email = email
super(email_logit, self).__init__(*args, **kwargs)

def notify(self):
# 发送一封email到self.email
# 这里就不做实现了
pass

从现在起,@email_logit 将会和 @logit 产生同样的效果,但是在打日志的基础上,还会多发送一封邮件给管理员。

函数装饰类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def wrapClass(cls):
def inner(a):
print('class name:', cls.__name__)
return cls(a)

return inner


@wrapClass
class Foo():
def __init__(self, name):
self.name = name

def fun(self):
print('self.name =', self.name)


Foo('小明')

# output:
# 'class name: Foo'

类装饰类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class ShowClassName(object):
def __init__(self, cls):
self._cls = cls

def __call__(self, a):
print('class name:', self._cls.__name__)
return self._cls(a)


@ShowClassName
class Foobar(object):
def __init__(self, a):
self.value = a

def fun(self):
print(self.value)


Foobar('小明')

# output:
# 'class name: Foobar'

以上。
都是Python的基础知识,希望各位能够掌握装饰器的基本用法。
– end –

看官,觉得好,就请喝杯咖啡吧!

-------------本文结束感谢您的阅读-------------