Python中装饰器的介绍与使用

作者: adm 分类: python 发布时间: 2023-11-11

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

二、装饰器使用的前置知识
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()

如果觉得我的文章对您有用,请随意赞赏。您的支持将鼓励我继续创作!