玩转 Python 装饰器

文章目录

    • 前言
    • 功能定位
    • 装饰器雏形
    • Pythonic 装饰器
    • 函数、类互相装饰
    • 上点难度:使用装饰器实现单例模式

前言

十天前,我发布了一个动态,分享关于 Python装饰器的 教程。
今天当我读到一些新的,关于 装饰器的应用 时,却怎么也想不起来它的原理。
于是,我又读了一遍教程,并尝试从应用的视角出发解释装饰器。下面是我的一些想法。

功能定位

首先,我们需要明确装饰器的功能。事实上,“装饰器”三字中的“装饰”就是其功能最好的浓缩。

在自然语言里,我们有类似的概念。比如:“会弹钢琴的女孩”中的“会弹钢琴”就是对“女孩”的修(装)饰。

我们希望在编程环境中做到以下两点:

  1. “女孩”可以看作一个庞大的抽象,需要很多的精力去培养、去构建。我们不希望在构建“会弹钢琴的女孩”时,侵入式修改“女孩”内部代码。
  2. 同时,“弹钢琴”是一个相对较小的,通用的爱好。它可以用到任何人身上,所以我们希望“弹钢琴”和“女孩”可以相对独立。

对此,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. 我们设装饰过后的 girlpiano_girl,可以如下实现:

def piano_girl():
    girl()
    print('I can play piano.')

piano_girl()

函数结构很简单,girl+piano=piano_girl。然而这种方式太过死板,仅针对这一种场景。换一个被修饰函数,比如 boy ,就需要重写整个函数。事实上,我们需要的是类似的加法结构:x + piano = piano_xx 是一个函数,是普罗大众,谁都可以学钢琴。也就是说,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()

这种形式就是最常见的装饰器模板。

Pythonic 装饰器

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()

主要改动有:

  1. 使用 wraps 装饰器,确保经过装饰的函数,函数名、文档注释等保持不变。(编程规范的一种)实现无痛装饰。
  2. @ 符号的引入,让装饰成为真正意义上的装饰,实现便捷装饰。

当然,还有更考究的做法,比如,怎么给装饰器引入参数等等,此处不再赘述。

函数、类互相装饰

上述装饰是基于最直白的,函数 装饰 函数,为模板的。其实 类 也可以做装饰器,也可以被装饰。下面抛砖引玉:

函数装饰类:

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))

各位读者可以结合上文做一下阅读理解,加油!

你可能感兴趣的:(python,开发语言)