likes
comments
collection
share

【python】装饰器详解

作者站长头像
站长
· 阅读数 13

前言

本文将带你学习装饰器在 Python 中的工作原理,如果在函数和类中使用装饰器,如何利用装饰器避免代码重复(DRY 原则,Don’t Repeat Yourself )。

装饰器是什么

装饰器一直以来都是 Python 中很有用、很经典的一个 feature,在工程中的应用也十分广泛,比如日志、缓存等等的任务都会用到。然而,在平常工作生活中,我发现不少人,尤其是初学者,常常因为其相对复杂的表示,对装饰器望而生畏,认为它“too fancy to learn”,实际并不如此。

你可能已经和装饰器打过不少交道了。在做面向对象编程时,我们就经常会用到 @staticmethod@classmethod 两个内置装饰器。此外,如果你接触过 click 模块,就更不会对装饰器感到陌生。click 最为人所称道的参数定义接口 @click.option(...) 就是利用装饰器实现的。

装饰器在 Python中是一个非常强大和有用的工具,因为它允许程序员修改函数或类的行为。装饰器允许我们包装另一个函数,以扩展包装函数的行为,而无需修改基础函数定义。这也被称为元编程,因为程序本身在程序运行时会尝试修改自身的另一部分。

装饰器是语法糖: 在代码中利用更简洁流畅的语法实现更为复杂的功能。

万能公式:注意理解语法糖的等价形式

我们知道,Python中一切皆对象。这意味着Python中的函数可以用作参数或作为参数传递。一等函数的属性:

  • 函数是 Object 类型的实例。
  • 可以将函数存储在变量中。
def func(message):
    print('Got a message: {}'.format(message))
    
send_message = func
send_message('hello world')

# 输出
Got a message: hello world
  • 可以将该函数作为参数传递给另一个函数。

def get_message(message):
    return 'Got a message: ' + message


def root_call(func, message):
    print(func(message))
    
root_call(get_message, 'hello world')

# 输出
Got a message: hello world
  • 我们可以在函数里定义函数,也就是函数的嵌套。
def func(message):
    def get_message(message):
        print('Got a message: {}'.format(message))
    return get_message(message)

func('hello world')

# 输出
Got a message: hello world
  • 函数的返回值也可以是函数对象(闭包)。
def func_closure():
    def get_message(message):
        print('Got a message: {}'.format(message))
    return get_message

send_message = func_closure()
send_message('hello world')

# 输出
Got a message: hello world
  • 可以将它们存储在数据结构中,例如哈希表,列表等。

装饰器语法糖

如果你接触Python有一段时间了的话,想必你对@符号一定不陌生了,没错@符号就是装饰器的语法糖。它放在一个函数开始定义的地方,它就像一顶帽子一样戴在这个函数的头上。和这个函数绑定在一起。在我们调用这个函数的时候,第一件事并不是执行这个函数,而是将这个函数做为参数传入它头顶上这顶帽子,这顶帽子我们称之为装饰函数装饰器

装饰器的使用方法很固定:

  • 先定义一个装饰函数(帽子)(也可以用类、偏函数实现)
  • 再定义你的业务函数、或者类(人)
  • 最后把这顶帽子戴在这个人头上

函数装饰器

decorator必须是一个“可被调用(callable)的对象或属性描述符(Descriptors)

理解描述符是深入理解 Python 的关键,因为它们是许多功能的基础,包括函数、方法、属性、类方法、静态方法和对超类的引用。这个暂不做过多赘述!

输入是函数,输出也是函数~

装饰不带参数的函数

def my_decorator(func):
    def wrapper():
        print('wrapper of decorator')
        func()
    return wrapper

def greet():
    print('hello world')

# 这里可以用下面的@语法糖实现,更优雅
greet = my_decorator(greet)
greet()

更优雅的语法糖@表示,大大提高函数的重复利用和程序的可读性:

def my_decorator(func):
    def wrapper():
        print('wrapper of decorator')
        func()
    return wrapper

@my_decorator
def greet():
    print('hello world')

greet()

输出结果为:

# 输出
wrapper of decorator
hello world

装饰带一个参数的函数

def my_decorator(func):
    def wrapper(message):
        print('wrapper of decorator')
        func(message)
    return wrapper


@my_decorator
def greet(message):
    print(message)


greet('hello world')

等价于:

def my_decorator(func):
    def wrapper(message):
        print('wrapper of decorator')
        func(message)

    return wrapper


def greet(message):
    print(message)

# @语法糖等价于下面这个
greet = my_decorator(greet)
greet('hello world')

输出结果为:

# 输出
wrapper of decorator
hello world

装饰带不定长参数的函数

def my_decorator(func):
    def wrapper(message, *args, **kwargs):  # 不定长参数*args,**kwargs
        print('wrapper of decorator')
        func(message, *args, **kwargs)

    return wrapper


@my_decorator
def greet(message):
    print(message)


greet('hello world')


@my_decorator
def greet2(message, num=2):
    print(message, num)


greet2("hello world2")

装饰器带参数

def repeat(num):
    def my_decorator(func):
        def wrapper(*args, **kwargs):
            for i in range(num):
                print('wrapper of decorator')
                func(*args, **kwargs)
        return wrapper
    return my_decorator


@repeat(4)
def greet(message):
    print(message)

# @语法糖等价于:
# my_decorator = repeat(4)
# greet = my_decorator(greet)

greet('hello world')

输出结果为:

# 输出:
wrapper of decorator
hello world
wrapper of decorator
hello world
wrapper of decorator
hello world
wrapper of decorator
hello world

原函数还是原函数吗?

我们试着打印出 greet() 函数的一些元信息:

greet.__name__
## 输出
'wrapper'

help(greet)
# 输出
Help on function wrapper in module __main__:

wrapper(*args, **kwargs)

为了解决这个问题,我们通常使用内置的装饰器@functools.wrap,它会帮助保留原函数的元信息(也就是将原函数的元信息,拷贝到对应的装饰器函数里)。

import functools

def my_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print('wrapper of decorator')
        func(*args, **kwargs)
    return wrapper
    
@my_decorator
def greet(message):
    print(message)

greet.__name__

# 输出
'greet'

类装饰器

定义一个类装饰器,装饰函数,默认调用__call__方法

类装饰器-本身无参数

class Count:
    def __init__(self, func):
        self.func = func
        self.num_calls = 0

    def __call__(self, *args, **kwargs):
        self.num_calls += 1
        print('num of calls is: {}'.format(self.num_calls))
        return self.func(*args, **kwargs)

@Count
def example():
    print("hello world")

# 等价于example = Count(example) 

example()

类装饰器本身无参数时等价于example = Count(example)

输出结果为:

# 输出
num of calls is: 1
hello world

example()

# 输出
num of calls is: 2
hello world

如何定义带参数的类装饰器

class Count:
    def __init__(self, a, *args, **kwargs): # 类装饰器参数
        self.a = a
        self.num_calls = 0

    def __call__(self, func): # 被装饰函数
        print(self.a)

        def wrapper(*args, **kwargs):
            print(self.a)
            self.num_calls += 1
            print('num of calls is: {}'.format(self.num_calls))
            return func(*args, **kwargs)
        return wrapper


@Count("aaaa")
def example():
    print("hello world")


print("开始调用example函数..............")

example()

等价于:

class Count:
    def __init__(self, a, *args, **kwargs):
        self.a = a
        self.num_calls = 0

    def __call__(self, func):
        print(self.a)

        def wrapper(*args, **kwargs):
            print(self.a)
            self.num_calls += 1
            print('num of calls is: {}'.format(self.num_calls))
            return func(*args, **kwargs)
        return wrapper


def example():
    print("hello world")

# @语法糖等价形式
example = Count("aaaa")(example)

print("开始调用example函数..............")

example()

输出结果为:

aaaa
开始调用example函数..............
aaaa
num of calls is: 1
hello world

描述符与装饰器

还有一类装饰器比较特殊,比如基于python描述符实现的property

在Python中,属性描述符可以用作装饰器,是因为描述符对象可以通过实现 __get__()__set__()__delete__() 方法来拦截对属性的访问和操作。 docs.python.org/3/reference… docs.python.org/3/howto/des…

关于描述符,这里不赘述。基于描述符,我们也可以实现自定义的property:

class cached_property:
    def __init__(self, func):
        self.func = func
        self.name = func.__name__
        self.cache = {}

    def __get__(self, instance, owner):
        if instance is None:
            return self
        if self.name not in self.cache:
            value = self.func(instance)
            self.cache[self.name] = value
        return self.cache[self.name]

    def __set__(self, instance, value):
        raise TypeError("can't set attribute")


class MyClass:
    @cached_property
    # @property
    def value(self):
        print("Calculating value...")
        return 42


obj = MyClass()
print(obj.value)  # 输出: Calculating value...   42

# 使用缓存的值
print(obj.value)  # 输出: 42

# 尝试修改属性值(抛出TypeError)
obj.value = "invalid"  # 抛出 TypeError: can't set attribute

装饰类的装饰器

在Python中,装饰类的装饰器是一种特殊类型的函数,它用于修改或增强类的行为。装饰器可以在不修改原始类定义的情况下,通过将类传递给装饰器函数来对其进行装饰。

通常情况下,装饰类的装饰器是一个接受类作为参数的函数,并返回一个新的类或修改原始类的函数。这个装饰器函数可以在类定义之前使用@符号应用到类上。

输入是类,输出也是类~

如何定义装饰类的装饰器

import time


def timer_decorator(cls):
    class TimerClass(cls):
        def __getattribute__(self, name):
            start_time = time.time()
            result = super().__getattribute__(name)
            end_time = time.time()
            execution_time = end_time - start_time
            print(f"Method '{name}' executed in {execution_time} seconds.")
            return result

    return TimerClass


@timer_decorator
class MyClass:
    def my_method(self):
        time.sleep(1)
        print("Executing my_method")


obj = MyClass()
obj.my_method()

输出结果为:

Method 'my_method' executed in 2.1457672119140625e-06 seconds.
Executing my_method

上述示例中,timer_decorator装饰器接收一个类作为参数,并返回一个继承自原始类的新类TimerClassTimerClass中重写了__getattribute__方法,在调用类的方法时,会计算方法的执行时间并进行打印。

巧用functools.partial

import time
import functools


class DelayFunc:
    def __init__(self,  duration, func):
        self.duration = duration
        self.func = func

    def __call__(self, *args, **kwargs):
        print(f'Wait for {self.duration} seconds...')
        time.sleep(self.duration)
        return self.func(*args, **kwargs)

    def eager_call(self, *args, **kwargs):
        print('Call without delay')
        return self.func(*args, **kwargs)


def delay(duration):
    """装饰器:推迟某个函数的执行。同时提供 .eager_call 方法立即执行
    """
    # 此处为了避免定义额外函数,直接使用 functools.partial 帮助构造
    # DelayFunc 实例
    return functools.partial(DelayFunc, duration)


@delay(duration=2)
def add(a, b):
    return a + b


# 这次调用将会延迟 2 秒
add(1, 2)
# 这次调用将会立即执行
add.eager_call(1, 2)

django示例

from django.contrib.auth.decorators import login_required

def require_login(view_class):
    # 使用@login_required装饰器对dispatch方法进行装饰
    view_class.dispatch = login_required(view_class.dispatch)
    return view_class

@require_login
class MyView(View):
    def get(self, request):
        # 处理GET请求的逻辑
        return HttpResponse("GET request")

    def post(self, request):
        # 处理POST请求的逻辑
        return HttpResponse("POST request")

等价于MyView = require_login(MyView)

objprint示例

一个有意思的python三方模块,使用装饰器打印object

from objprint import add_objprint

class Position:
    def __init__(self, x, y):
        self.x = x
        self.y = y

@add_objprint
class Player:
    def __init__(self):
        self.name = "Alice"
        self.age = 18
        self.items = ["axe", "armor"]
        self.coins = {"gold": 1, "silver": 33, "bronze": 57}
        self.position = Position(3, 5)

# This will print the same thing as above
print(Player()) 

输出为:

<Player
  .name = 'Alice',
  .age = 18,
  .items = ['axe', 'armor'],
  .coins = {'gold': 1, 'silver': 33, 'bronze': 57},
  .position = <Position
    .x = 3,
    .y = 5
  >
> 

使用 wrapt 模块编写更扁平的装饰器

在写装饰器的过程中,你有没有碰到过什么不爽的事情?这里列举两个可能使你特别难受的点:

  1. 实现带参数的装饰器时,层层嵌套的函数代码特别难写、难读
  2. 因为函数和类方法的不同,为前者写的装饰器经常没法直接套用在后者上
import random


def provide_number(min_num, max_num):
    """装饰器:随机生成一个在 [min_num, max_num] 范围的整数,追加为函数的第一个位置参数
    """
    def wrapper(func):
        def decorated(*args, **kwargs):
            num = random.randint(min_num, max_num)
            # 将 num 作为第一个参数追加后调用函数
            return func(num, *args, **kwargs)
        return decorated
    return wrapper
    


@provide_number(1, 100)
def print_random_number(num):
    print(num)

# 输出 1-100 的随机整数
# OUTPUT: 72
print_random_number()

@provide_number 装饰器功能看上去很不错,但它有着我在前面提到的两个问题:嵌套层级深、无法在类方法上使用。如果直接用它去装饰类方法,会出现下面的情况:

class Foo:
    @provide_number(1, 100)
    def print_random_number(self, num):
        print(num)

# OUTPUT: <__main__.Foo object at 0x104047278>
Foo().print_random_number()

Foo 类实例中的 print_random_number 方法将会输出类实例 self ,而不是我们期望的随机数 num

之所以会出现这个结果,是因为类方法 (method) 和函数 (function) 二者在工作机制上有着细微不同。如果要修复这个问题,provider_number 装饰器在修改类方法的位置参数时,必须聪明的跳过藏在 *args 里面的类实例 self 变量,才能正确的将 num 作为第一个参数注入。

这时,就应该是 wrapt 模块闪亮登场的时候了。wrapt 模块是一个专门帮助你编写装饰器的工具库。利用它,我们可以非常方便的改造 provide_number 装饰器,完美解决“嵌套层级深”和“无法通用”两个问题,

import random

import wrapt


def provide_number(min_num, max_num):
    @wrapt.decorator
    def wrapper(wrapped, instance, args, kwargs):
        # 参数含义:
        #
        # - wrapped:被装饰的函数或类方法
        # - instance:
        #   - 如果被装饰者为普通类方法,该值为类实例
        #   - 如果被装饰者为 classmethod 类方法,该值为类
        #   - 如果被装饰者为类/函数/静态方法,该值为 None
        #
        # - args:调用时的位置参数(注意没有 * 符号)
        # - kwargs:调用时的关键字参数(注意没有 ** 符号)
        #
        num = random.randint(min_num, max_num)
        # 无需关注 wrapped 是类方法或普通函数,直接在头部追加参数
        args = (num,) + args
        return wrapped(*args, **kwargs)

    return wrapper


@provide_number(1, 100)
def print_random_number(num):
    print(num)


class Foo:
    @provide_number(1, 100)
    def print_random_number(self, num):
        print(num)


# 输出 1-100 的随机整数
print_random_number()

Foo().print_random_number()

使用wrapt写一个不带参数的装饰器

import wrapt

@wrapt.decorator
def pass_through(wrapped, instance, args, kwargs):
    return wrapped(*args, **kwargs)

@pass_through
def function():
    pass

使用wrapt写一个带参数的装饰器

如果您希望实现一个接受参数的装饰器,请将装饰器的定义包装在函数闭包中。应用装饰器时提供给外部函数的任何参数都将在调用包装函数时可供内部包装器使用。

函数签名是固定的,必须是(wrapped, instance, args, kwargs)

import wrapt

def with_arguments(myarg1, myarg2):
    @wrapt.decorator
    def wrapper(wrapped, instance, args, kwargs):
        return wrapped(*args, **kwargs)
    return wrapper

@with_arguments(1, 2)
def function():
    pass

使用wrapt的装饰器嵌套

import wrapt


@wrapt.decorator
def uppercase(wrapped, instance, args, kwargs):
    result = wrapped(*args, **kwargs)
    if isinstance(result, str):
        return result.upper()
    return result


def repeat(n):
    @wrapt.decorator
    def wrapper(wrapped, instance, args, kwargs):
        return [wrapped(*args, **kwargs) for _ in range(n)]

    return wrapper


@repeat(3)
@uppercase
def greet(name):
    return f"Hello, {name}!"


print(greet("Alice"))  # Output: ['HELLO, ALICE!', 'HELLO, ALICE!', 'HELLO, ALICE!']

使用 wrapt 模块编写的装饰器,相比原来拥有下面这些优势:

  • 嵌套层级少:使用 @wrapt.decorator 可以将两层嵌套减少为一层
  • 更简单:处理位置与关键字参数时,可以忽略类实例等特殊情况
  • 更灵活:针对 instance 值进行条件判断后,更容易让装饰器变得通用

装饰器的嵌套

import functools

def my_decorator1(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print('execute decorator1')
        func(*args, **kwargs)
    return wrapper


def my_decorator2(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print('execute decorator2')
        func(*args, **kwargs)
    return wrapper


@my_decorator1
@my_decorator2
def greet(message):
    print(message)


greet('hello world')

它的执行顺序从里到外,所以上面的语句也等效于下面这行代码:

greet = my_decorator1(my_decorator2(greet))

# 或者
# greet = my_decorator2(greet)
# greet = my_decorator1(greet)

输出结果为:

# 输出
execute decorator1
execute decorator2
hello world

多装饰器的执行顺序

说到Python装饰器的执行顺序,有很多半吊子张口就来:

靠近函数名的装饰器先执行,远离函数名的装饰器后执行。

这种说法是不准确的

举个栗子

def decorator_outer(func):
    print("我是外层装饰器")
    print('a')
    print('b')
    def wrapper():
        print('外层装饰器,函数运行之前')
        func()
        print('外层装饰器,函数运行之后')
    print('外层装饰器闭包初始化完毕')
    print('c')
    print('d')
    return wrapper

def decorator_inner(func):
    print("我是内层装饰器")
    print(1)
    print(2)
    def wrapper():
        print('内层装饰器,函数运行之前')
        func()
        print('内层装饰器,函数运行之后')
    print('内层装饰器闭包初始化完毕')
    print(3)
    print(4)
    return wrapper  

@decorator_outer
@decorator_inner
def func():
    print("我是函数本身")

func()

在这里,你可以先花几秒钟思考下这段代码的输出结果是什么呢?也许会出乎一些人的预料!!

结果揭晓

我是内层装饰器
1
2
内层装饰器闭包初始化完毕
3
4
我是外层装饰器
a
b
外层装饰器闭包初始化完毕
c
d
# ==================================================
外层装饰器,函数运行之前
内层装饰器,函数运行之前
我是函数本身
内层装饰器,函数运行之后
外层装饰器,函数运行之后

其实,只要我们套用万能替代公式,是不难得出正确的答案的。直接上代码:

def decorator_outer(func):
    print("我是外层装饰器")
    print('a')
    print('b')

    def wrapper():
        print('外层装饰器,函数运行之前')
        func()
        print('外层装饰器,函数运行之后')

    print('外层装饰器闭包初始化完毕')
    print('c')
    print('d')
    return wrapper


def decorator_inner(func):
    print("我是内层装饰器")
    print(1)
    print(2)

    def wrapper():
        print('内层装饰器,函数运行之前')
        func()
        print('内层装饰器,函数运行之后')

    print('内层装饰器闭包初始化完毕')
    print(3)
    print(4)
    return wrapper


# @decorator_outer
# @decorator_inner
def func():
    print("我是函数本身")


#
# func()

func = decorator_inner(func)
print("----------------------------------------")
func = decorator_outer(func)
print("==================================================")
func()

装饰器里面的代码中,wrapper闭包外面的代码确实是内层装饰器先执行,外层装饰器后执行。这部分是在带上@帽子之后就执行了,而并非是在调用的时候。这个从等价形式也可以得出结论,因为带帽的时候其实已经做过某些调用了,这个你可以细品。

重点是闭包wrapper内部的代码的执行顺序。通过等价公式不难得出,最后执行的func已经不是原来的func函数,而是decorator_outer(func)

  • 所以执行func()其实是执行了decorator_outer(func)(),因此先打印了外层装饰器,函数运行之前;
  • 然后执行decorator_outer装饰器wrapper闭包里的func函数,而decorator_outer装饰器wrapper闭包里的func函数此时是func = decorator_inner(func);
  • 所以紧接着打印了内层装饰器,函数运行之前—>我是函数本身—>内层装饰器,函数运行之后—>外层装饰器,函数运行之后

所以,当我们说多个装饰器堆叠的时候,哪个装饰器的代码先运行时,不能一概而论说内层装饰器的代码先运行。

闭包wrapper内部的代码执行逻辑:

  1. 外层装饰器先执行,但只执行了一部分,执行到调用func()
  2. 内层装饰器开始执行
  3. 内层装饰器执行完
  4. 外层装饰器执行完

重点:需要搞清楚函数和函数调用的区别,注意:函数是可以当成返回值的

在实际应用的场景中,当我们采用上面的方式写了两个装饰方法比如先验证有没有登录@login_required,再验证权限够不够时@permision_allowed时,我们采用下面的顺序来装饰函数:

def login_required(func):
    def wrapper(*args, **kwargs):
        print('检测是否有特定的Cookies')
        is_login = False
        if not is_login:
            return {'success': False, "msg": "没有登录"}
        return func(*args, **kwargs)
    return wrapper


def permision_allowed(func):
    def wrapper(*args, **kwargs):
        print('检测是否有特定的数据集权限')
        print('首先从请求参数中获取dataset_id')
        print('然后从登录session中获取用户id,注意,如果没有登录,是没有session的')
        print('判断用户是否有这个dataset的权限')
        has_data_set_permission = True
        if not has_data_set_permission:
            return {'success': False, "msg": "没有数据集权限"}
        return func(*args, **kwargs)
    return wrapper

@login_required
@permision_allowed
def f()
  # Do something
  return

装饰器应用场景示例

缓存装饰器

下面的缓存装饰器可以帮助你避免重复计算,以提高代码的性能。

def cache_decorator(func):
    # 创建一个字典来存储缓存的结果
    cache = dict()

    # 定义内部包装函数,用于接收任意数量的位置参数

    def wrapper(*args):
        # 检查当前参数是否在缓存中
        if args in cache:
            # 如果在缓存中,则从缓存中获取结果并打印提示信息
            print(f"从缓存中获取{args}的结果")
            return cache[args]
        # 如果不在缓存中,则调用原始函数计算结果
        result = func(*args)
        # 将计算结果存储到缓存中,并打印提示信息
        cache[args] = result
        print(f"计算{args}的结果并将其存入缓存")
        # 返回计算结果
        return result

    # 返回包装函数
    return wrapper


# 使用缓存装饰器修饰fibonacci函数
@cache_decorator
def fibonacci(n):
    if n < 2:
        return n
    else:
        return fibonacci(n - 1) + fibonacci(n - 2)


print(fibonacci(3))
print("*****************")
print(fibonacci(10))

输入合法性检查

# 定义类型检查装饰器
def type_check_decorator(func):
    # 定义内部包装函数,用于接收任意数量的位置参数和关键字参数

    def wrapper(*args, **kwargs):
        # 遍历位置参数
        for i, arg in enumerate(args):
            # 如果参数不是字符串类型,抛出TypeError异常
            if not isinstance(arg, str):
                raise TypeError(f"第{i + 1}个参数值{arg}必须是str类型")
        # 遍历关键字参数
        for key, value in kwargs.items():
            # 如果关键字参数的值不是字符串类型,抛出TypeError异常
            if not isinstance(value, str):
                raise TypeError(f"关键字参数{key}必须是str类型")
        # 参数检查通过后,调用原始函数并返回结果
        return func(*args, **kwargs)

    # 返回包装函数
    return wrapper


# 使用类型检查装饰器修饰concat_strings函数
@type_check_decorator
def concat_strings(*strings, sep="-"):
    return sep.join(strings)


print(concat_strings("1", 3, "5", sep=""))

日志记录

import time
import functools

def log_execution_time(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        res = func(*args, **kwargs)
        end = time.perf_counter()
        print('{} took {} ms'.format(func.__name__, (end - start) * 1000))
        return res
    return wrapper
    
@log_execution_time
def calculate_similarity(items):
    ...

身份认证

import functools

def authenticate(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        request = args[0]
        if check_user_logged_in(request): # 如果用户处于登录状态
            return func(*args, **kwargs) # 执行函数post_comment() 
        else:
            raise Exception('Authentication failed')
    return wrapper
    
@authenticate
def post_comment(request, ...)
    ...
 

单例模式

def singleton(cls):
    instances = {}

    def wrapper(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    
    return wrapper

@singleton
class Logger:
    def __init__(self):
        self.log_file = open("log.txt", "a")

    def log(self, message):
        self.log_file.write(f"{message}\n")

在上述示例中,singleton 装饰器用于创建单例类。装饰器包装了 Logger 类,确保只有一个 Logger 的实例存在。

策略模式

电商领域有个功能明显可以使用“策略”模式,即根据客户的属性或订单中的商品计算折扣。 假如一个网店制定了下述折扣规则。

  • 1000或以上积分的顾客,每个订单享5% 折扣。
  • 同一订单中,单个商品的数量达到20个或以上,享10%折扣。
  • 订单中的不同商品达到10个或以上,享7%折扣。

简单起见,我们假定一个订单一次只能享用一个折扣

from collections import namedtuple

promos = []


def promotion(promo_func):
    promos.append(promo_func)
    return promo_func


@promotion
def fidelity(order):
    """为积分为1000或以上的顾客提供5%折扣"""
    return order.total() * .05 if order.customer.fidelity >= 1000 else 0


@promotion
def bulk_item(order):
    """单个商品为20个或以上时提供10%折扣"""
    discount = 0
    for item in order.cart:
        if item.quantity >= 20:
            discount += item.total() * .1
    return discount


@promotion
def large_order(order):
    """订单中的不同商品达到10个或以上时提供7%折扣"""
    distinct_items = {item.product for item in order.cart}
    if len(distinct_items) >= 10:
        return order.total() * .07
    return 0


def best_promo(order):
    """选择可用的最佳折扣"""
    return max(promo(order) for promo in promos)


Customer = namedtuple('Customer', 'name fidelity')


class LineItem:
    def __init__(self, product, quantity, price):
        self.product = product
        self.quantity = quantity
        self.price = price

    def total(self):
        return self.price * self.quantity


class Order:  # 上下文
    def __init__(self, customer, cart, promotions=None):
        self.__total = None
        self.customer = customer
        self.cart = list(cart)
        self.promotion = promotions

    def total(self):
        if not hasattr(self, '__total'):
            self.__total = sum(item.total() for item in self.cart)
        return self.__total


joe = Customer('John Doe', 1000)

joe_cart = [LineItem('banana', 4, 5.0), LineItem('apple', 10, 1.5), LineItem('watermellon', 5, 5.0)]

joe_order = Order(joe, joe_cart)

print(best_promo(joe_order))

总结

所谓的装饰器,其实就是通过装饰器函数,来修改原函数的一些功能,使得原函数不需要修改。

一切callable的对象都可以被用来实现装饰器。

wrapt模块很有用,用它可以帮助我们用更简单的代码写出复杂装饰器。

装饰器的应用场景其实很常见,我们常见的判断用户是否登录(token校验的判断)、用户是否有访问权限很多都是使用装饰器来判断的,在DRF(django restframework)中的@api_view@permission_classes

合理使用装饰器,往往能极大地提高程序的可读性以及运行效率。

每当你对装饰器感到迷茫的时候,可以将装饰器用其等价形式理解。

参考

mp.weixin.qq.com/s/PXu-68puF… segmentfault.com/a/119000000… github.com/piglei/one-…

喜欢这篇文章的话,就点个关注吧,或者关注一下我的公众号『海哥python』也可以,会持续分享高质量Python文章,以及其它内容。