十天前,我发布了一个动态,分享关于 Python装饰器的 教程。
今天当我读到一些新的,关于 装饰器的应用 时,却怎么也想不起来它的原理。
于是,我又读了一遍教程,并尝试从应用的视角出发解释装饰器。下面是我的一些想法。
首先,我们需要明确装饰器的功能。事实上,“装饰器”三字中的“装饰”就是其功能最好的浓缩。
在自然语言里,我们有类似的概念。比如:“会弹钢琴的女孩”中的“会弹钢琴”就是对“女孩”的修(装)饰。
我们希望在编程环境中做到以下两点:
对此,Python 提供了一个很有特色的解决方案,那就是 Python 装饰器。它在使用时十分轻便,只需在原抽象上添加一行代码即可实现抽象功能的拓展。如下:
def girl():
pass
###############
@play_piano
def girl():
pass
我们希望 girl
函数在经过装饰以后,能够实现功能拓展,就像“女孩”在经过修饰后变成了“会弹钢琴的女孩”一样。
下面我们逐步去实现这样的目的。
我们首先定义一下两个功能函数:
def girl():
print('I am a girl.')
def play_piano():
print('I can play piano.')
这两个函数是独立的,调用 girl
函数时不会出现 piano
. 我们设装饰过后的 girl
为 piano_girl
,可以如下实现:
def piano_girl():
girl()
print('I can play piano.')
piano_girl()
函数结构很简单,girl+piano=piano_girl
。然而这种方式太过死板,仅针对这一种场景。换一个被修饰函数,比如 boy
,就需要重写整个函数。事实上,我们需要的是类似的加法结构:x + piano = piano_x
。x
是一个函数,是普罗大众,谁都可以学钢琴。也就是说,x
是一个需要被传进来的参数,它在形式上是一个函数。
def girl():
print('I am a girl.')
def boy():
print('I am a boy.')
def piano_people(people):
people()
print('I can play piano.')
piano_people(girl)
piano_people(boy)
至此,我们的 piano_people
可以算作一个广义上的装饰器了,但 “装饰” 二字仍然不够突出。试想一下,女孩逛街买了耳坠,我们说 “带了耳坠的女孩” 。这更符合平时语境下的 “装饰”,即 “装饰” 是一个动词,重在描述过程。
上述代码是 x + piano = piano_x
并执行了 piano_x
。而我们更希望的是,给定 x
,求解 装饰后的函数 piano_x
,这个求解的过程是装饰器的主要功能。上述代码微调即可:
def girl():
print('I am a girl.')
def boy():
print('I am a boy.')
def play_piano(people):
def piano_people():
people()
print('I can play piano.')
return piano_people
piano_girl = play_piano(girl)
piano_girl()
这种形式就是最常见的装饰器模板。
Python 风格的装饰器对上述模板做了极致的优化,如下:
from functools import wraps
def play_piano(people):
@wraps(people)
def piano_people():
people()
print('I can play piano.')
return piano_people
@play_piano
def girl():
print('I am a girl.')
girl()
主要改动有:
wraps
装饰器,确保经过装饰的函数,函数名、文档注释等保持不变。(编程规范的一种)实现无痛装饰。@
符号的引入,让装饰成为真正意义上的装饰,实现便捷装饰。当然,还有更考究的做法,比如,怎么给装饰器引入参数等等,此处不再赘述。
上述装饰是基于最直白的,函数 装饰 函数,为模板的。其实 类 也可以做装饰器,也可以被装饰。下面抛砖引玉:
函数装饰类:
from functools import wraps
def add_2_dec(cls):
@wraps(cls)
def add_2(*args, **kwargs):
an_instance = cls(*args, **kwargs)
an_instance.age = an_instance.age + 2
return an_instance
return add_2
@add_2_dec
class girl():
def __init__(self, age):
self.age = age
a_girl = girl(age=20)
print(a_girl.age)
类装饰函数:
(需要注意的是,类装饰器起作用的是 __call__
魔法函数,这个函数等价于 装饰器雏形
那节提到的 piano_people
)
from functools import wraps
class play_piano():
def __init__(self, people):
@wraps(people)
def piano_people():
people()
print('I can play piano.')
self.piano_people = piano_people
def __call__(self):
return self.piano_people()
@play_piano
def girl():
print('I am a girl.')
girl()
类装饰类:
from functools import wraps
class add_2_dec():
def __init__(self, cls):
@wraps(cls)
def get_new_instance(*args, **kwargs):
an_instance = cls(*args, **kwargs)
an_instance.age = an_instance.age + 2
return an_instance
self.get_new_instance = get_new_instance
def __call__(self, *args, **kwargs):
return self.get_new_instance(*args, **kwargs)
@add_2_dec
class girl():
def __init__(self, age):
self.age = age
a_girl = girl(age=20)
print(a_girl.age)
单例模式是最简单的设计模式。它强调,某些特殊的类在整个程序运行中仅能存在一个实例。这要求我们每次初始化时,检查是否存在已有的实例。借助 Python 语言中的装饰器可以轻松实现这样的特性。下面代码摘自Python单例模式(Singleton)的N种实现 - 知乎 (zhihu.com)
函数装饰类:
Cls 在经过装饰后就变成了 singleton 中的 inner 函数,原有的 Cls 存放在了 变量 cls 里。由于类在定义时会记住上层命名空间的变量,所以 _instance
会被记住。(这段代码不理解的,可以补充一下闭包相关的知识)
之后每次新建 Cls 类都是执行 inner 函数,又因为闭包函数的特性,不会新建
def singleton(cls):
_instance = {}
def inner():
if cls not in _instance:
_instance[cls] = cls()
return _instance[cls]
return inner
@singleton
class Cls(object):
def __init__(self):
pass
cls1 = Cls()
cls2 = Cls()
print(id(cls1) == id(cls2))
类装饰类:
class Singleton(object):
def __init__(self, cls):
self._cls = cls
self._instance = {}
def __call__(self):
if self._cls not in self._instance:
self._instance[self._cls] = self._cls()
return self._instance[self._cls]
@Singleton
class Cls2(object):
def __init__(self):
pass
cls1 = Cls2()
cls2 = Cls2()
print(id(cls1) == id(cls2))
各位读者可以结合上文做一下阅读理解,加油!