【python】装饰器详解

前言

本文将带你学习装饰器在 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)的对象

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

装饰不带参数的函数

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

类装饰器

类装饰器-本身无参数

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

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

输入是类,输出也是类~

如何定义装饰类的装饰器

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 模块编写的装饰器,相比原来拥有下面这些优势:

  • 嵌套层级少:使用 @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

装饰器应用场景示例

输入合法性检查

import functools













def validation_check(input):
    @functools.wraps(func)



    def wrapper(*args, **kwargs): 
        ... # 检查输入是否合法
    
@validation_check
def neural_network_training(param1, param2, ...):
    ...

日志记录

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

总结

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

一切 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文章,以及其它内容。

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYHq3dlI' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片