Python中装饰器的介绍与使用

一、装饰器的定义

装饰器,顾名思义,就是起到装饰的作用,即在不改变已有函数代码及其调用方式的前提下,对已有函数进行功能扩展,实现了低侵入性、高内聚低耦合的目标。

二、装饰器使用的前置知识

2.1 Python中函数的使用

和其它编程语言不一样,Python中的函数可以一次性返回多个值(tuple),而且函数可以被引用,赋值给其它变量甚至是函数名。

def hello():
    print("hello")

def world():
    print("world")

# 函数名可以重新被赋值,导致函数名和函数内容的置换
hello,world = world, hello
# world
hello()
# hello
world()

在Python中,函数是一等公民,可以像普通变量一样被赋值、被引用、被当作其它函数的入参、被当作其它函数的返回值。

def say_hello(name):
    print(f"Hello, {name}")

def say_hi(name):
    print(f"Hi, {name}")

def greet_job(func, name):
    func(name)

# 函数被当作其它函数的入参
# Hello, Jack
greet_job(say_hello, "Jack")
# Hi, Jack
greet_job(say_hi, "Jack")

2.2 Python中的内部函数

Python允许在一个函数中定义另外一个函数,即实现函数的嵌套,内部函数具有如下的特性:

  • 能够访问所有外层函数中的所有资源;
  • 该内部函数仅在其直接外层函数中可见;
def parent():
    print("parent函数执行")

    local_var = "zhangsan"

    def first_child():
        print(f"first_child函数执行,可以访问到外层函数的资源:{local_var}")

    def second_child():
        print(f"second_child函数执行,可以访问到外层函数的资源:{local_var}")

    # 内部函数仅在其直接外层函数中可见
    first_child()
    second_child()

parent()

三、装饰器的使用

3.1 简单装饰器的使用

# 定义装饰器,该装饰器什么也不做,把入参中的函数原封不动地返回
def my_decorator(func):
    return func

def say_hello():
    print("Hello")

# 使用装饰器装饰一下原函数
say_hello = my_decorator(say_hello)
say_hello()

如上是最简单的一个例子,装饰器什么也没做,只是把入参的函数原封不动地返回,一般而言,装饰器肯定会有所内容地:

def my_decorator(func):
    print("装饰器函数被调用了")
    # 对原函数进行装饰
    def wrapper():
        print(f"在{func.__name__}之前做点事情...")
        func()
        print(f"在{func.__name__}之后做点事情...")
    # 将装饰后的函数返回给原函数的引用,更新原函数
    return wrapper

def say_hello():
    print("Hello")

say_hello = my_decorator(say_hello)
say_hello()

输出内容如下:

装饰器函数被调用了
在say_hello之前做点事情...
Hello
在say_hello之后做点事情...

3.2 使用装饰器语法糖

通过如上装饰器的简单使用,我们其实知道了其本质,就是把被装饰的原函数当作参数传入装饰器,然后对其装饰一番,再返回赋值给原函数的引用,从而在原函数执行的时候,顺带执行装饰的代码。

然后如上的写法不够简洁,不够优雅,所以Python为我们提供了语法糖写法:

def my_decorator(func):
    print("装饰器函数被调用了")
    # 对原函数进行装饰
    def wrapper():
        print(f"在{func.__name__}之前做点事情...")
        func()
        print(f"在{func.__name__}之后做点事情...")
    # 将装饰后的函数返回给原函数的引用,更新原函数
    return wrapper

@my_decorator
def say_hello():
    print("Hello")

say_hello()

其实就是使用@@my_decorator替换了一句say_hello = my_decorator(say_hello),但确实看上去简洁优雅多了。

3.3 装饰器的内部函数

我们在上面的例子中,装饰器内部又定义了一个wrapper函数,那么装饰逻辑分布在wrapper的外部和在内部的区别是什么呢?

其实我们最终返回的是对原函数装饰过后的wrapper函数,所以:

  • wrapper内部的装饰逻辑,只会在原函数调用的时候才会执行;
  • wrapper外部的装饰逻辑,只要装饰器生效就会执行;
def my_decorator(func):
    # 无论原函数执行与否,只要装饰器使用了,就会被执行
    print("装饰器函数被调用了")
    # 对原函数进行装饰
    # 只有当原函数执行时,内部的装饰逻辑才会执行
    def wrapper():
        print(f"在{func.__name__}之前做点事情...")
        func()
        print(f"在{func.__name__}之后做点事情...")
    # 将装饰后的函数返回给原函数的引用,更新原函数
    return wrapper

3.4 带参数的原函数

为了能在装饰器中给原函数传入参数,我们不得不在定义wrapper内部函数的时候,增加一个入参,如下所示:

def my_decorator(func):
    print("装饰器函数被调用了")
    # 对原函数进行装饰
    def wrapper(name):
        print(f"在{func.__name__}之前做点事情...")
        func(name)
        print(f"在{func.__name__}之后做点事情...")
    # 将装饰后的函数返回给原函数的引用,更新原函数
    return wrapper

@my_decorator
def say_hello(name):
    print(f"Hello,{name}")

say_hello("Jack")

但是这样有一个缺点,这个装饰器几乎只对这个方法有用,对其它方法,如果参数不匹配的话就没法使用了,所以我们通常结合Python中的变长参数一起使用:

def my_decorator(func):
    print("装饰器函数被调用了")
    # 对原函数进行装饰,*args表示多个顺序参数,**kwargs表示多个字典参数
    def wrapper(*args, **kwargs):
        print(f"在{func.__name__}之前做点事情...")
        func(*args, **kwargs)
        print(f"在{func.__name__}之后做点事情...")
    # 将装饰后的函数返回给原函数的引用,更新原函数
    return wrapper

@my_decorator
def say_hello(name, age=10):
    print(f"Hello,{name}, your age is: {age}")

say_hello("Jack", 12)

这样改造之后,其它任意函数都可以使用我们的装饰器了。

3.5 带参数的装饰器

我们在如上案例中使用装饰器时,装饰器并没有入参,其实Python也是支持的,只不过需要将装饰器再封装一层:

def my_repeat(times):
    def my_decorator(func):
        def wrapper(*args, **kwargs):
            print(f"在{func.__name__}之前做点事情...")
            # 闭包函数,使用了其外层函数的资源times
            for n in range(times):
                func(*args, **kwargs)
            print(f"在{func.__name__}之后做点事情...")
        # 将装饰后的函数返回给原函数的引用,更新原函数
        return wrapper
    return my_decorator

@my_repeat(3)
def say_hello(name, age=10):
    print(f"Hello,{name}, your age is: {age}")

# 原函数调用时,其装饰函数外层的函数上下文和资源仍然存在
say_hello("Jack", 12)

输出结果为:

在say_hello之前做点事情...
Hello,Jack, your age is: 12
Hello,Jack, your age is: 12
Hello,Jack, your age is: 12
在say_hello之后做点事情...

其实@@my_repeat(3)就等价于:

my_decorator = my_repeat(3)
say_hello = my_decorator(say_hello)

3.6 装饰器装饰类

在上面的使用中,我们都是用来装饰函数,那么装饰类是什么意思,如何使用呢?

相较于函数装饰器,类装饰器具有灵活度大、高内聚、封装性的优点,此时主要依靠类的__call__方法来实现对原函数的装饰逻辑。

class Foo(object):
    def __init__(self, func):
        self._func = func

    def __call__(self):
        print(f"在{self._func.__name__}之前做点事情")
        self._func()
        print(f"在{self._func.__name__}之后做点事情")

@Foo
def bar():
    print("原函数执行的逻辑....")

bar()

此时的@Foo就等价于bar = Foo(bar)

3.7 装饰器的执行顺序

当有多个装饰器装饰同一个函数的时候,各个装饰器的执行顺序会是怎么样的呢?

def my_decorator1(func):
    def wrapper(*args, **kwargs):
        print(f"my_decorator1在{func.__name__}之前做点事情...")
        func(*args, **kwargs)
        print(f"my_decorator1在{func.__name__}之后做点事情...")
    return wrapper

def my_decorator2(func):
    def wrapper(*args, **kwargs):
        print(f"my_decorator2在{func.__name__}之前做点事情...")
        func(*args, **kwargs)
        print(f"my_decorator2在{func.__name__}之后做点事情...")
    return wrapper

def my_decorator3(func):
    def wrapper(*args, **kwargs):
        print(f"my_decorator3在{func.__name__}之前做点事情...")
        func(*args, **kwargs)
        print(f"my_decorator3在{func.__name__}之后做点事情...")
    return wrapper

@my_decorator1
@my_decorator2
@my_decorator3
def say_hello(name, age=10):
    print(f"Hello,{name}, your age is: {age}")

say_hello("Jack", 12)

输出结果如下:

my_decorator1在wrapper之前做点事情...
my_decorator2在wrapper之前做点事情...
my_decorator3在say_hello之前做点事情...
Hello,Jack, your age is: 12
my_decorator3在say_hello之后做点事情...
my_decorator2在wrapper之后做点事情...
my_decorator1在wrapper之后做点事情...

四、装饰器的应用

Python自带了三个原生的装饰器,下面逐一进行介绍。

4.1 @property

当我们需要访问对象中的属性的时候,对于公开的属性,可以直接访问,对于私有的属性,可以通过get和set方法来访问,如下所示:

class Person(object):
    def __init__(self, name, age):
        self.name = name
        self._age = age
        
    # _age是私有属性,必须通过方法来访问
    def get_age(self):
        return self._age
    
    # _age是私有属性,必须通过方法来修改
    def set_age(self, age):
        self._age = age

    def __call__(self):
        # 对象内部可以不受限制地访问自身的属性
        print('姓名:{},年龄:{}'.format(self.name,self._age))

jack = Person("jack", 20)
print(f"jack的姓名为:{jack.name},年龄为:{jack.get_age()}")
jack.set_age(35)
print(f"jack的姓名为:{jack.name},年龄为:{jack.get_age()}")
jack()

输出内容为:

jack的姓名为:jack,年龄为:20
jack的姓名为:jack,年龄为:35
姓名:jack,年龄:35

但是每次需要访问对象的私有属性,都要通过set和get有点麻烦,所以Python提供了@property来将这些方法变成可以直接读写的属性:

class Person(object):
    def __init__(self, name, age):
        self.name = name
        self._age = age

    @property
    def age(self):
        return self._age
    
    def set_age(self, age):
        self._age = age

    def __call__(self):
        print('姓名:{},年龄:{}'.format(self.name,self._age))

jack = Person("jack", 20)
# 可以以读取属性的方式来调用对象的方法
print(f"jack的姓名为:{jack.name},年龄为:{jack.age}")
jack.set_age(35)
print(f"jack的姓名为:{jack.name},年龄为:{jack.age}")
jack()

如此,我们既可以维持原来保护私有属性的原则,又可以像读取公开属性一样获取其值。

4.2 @classmethod

一般情况下,如果我们想调用某个类的方法的话,首先需要实例化一个对象,然后使用对象来调用这个方法:

class Animal(object):
    name = "tom"
    def eat(self):
        print(f"{self.name}正在吃东西")
    def sleep(self):
        print(f"{self.name}正在睡觉")
    def make_noise(cls):
         print("miao~miao~")

cat = Animal()
cat.eat()
cat.sleep()
cat.make_noise()

但是使用@classmethod就能让我们可以直接通过类名来调用方法,不需要实例化类的对象了:

class Animal(object):
    name = "tom"
    @classmethod
    def eat(cls):
        print(f"{cls.name}正在吃东西")
    @classmethod
    def sleep(cls):
        print(f"{cls.name}正在睡觉")
    @classmethod
    def make_noise(cls):
        print("miao~miao~")

# 完全不需要实例化对象,直接可以调用方法
Animal.eat()
Animal.sleep()
Animal.make_noise()

4.3 @staticmethod

@staticmethod的用法和@classmethod的用法基本类似,区别是被装饰的原函数中不需要cls入参。

class Animal(object):
    name = "tom"
    @staticmethod
    def eat():
        print(f"{Animal.name}正在吃东西")
    @classmethod
    def sleep(cls):
        print(f"{cls.name}正在睡觉")
    @classmethod
    def make_noise(cls):
        print("miao~miao~")

Animal.eat()
Animal.sleep()
Animal.make_noise()
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,002评论 6 509
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,777评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,341评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,085评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,110评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,868评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,528评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,422评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,938评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,067评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,199评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,877评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,540评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,079评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,192评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,514评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,190评论 2 357

推荐阅读更多精彩内容