[python3] python装饰器 简明教程

过去看了很多博客,都感觉写得不很友好,自己写一篇,从基础讲起,结合代码,帮助读者快速掌握这个python利器。

python用了一段时间之后,就会发现有些重复的代码反复出现在各个函数中,有没有办法让这些函数“一键”增加功能呢?

python装饰器可以很好地解决这个问题。

目录

0.预备知识

0.1 函数指针

0.2 函数指针在python中的用法

1.最简单的无参数的函数装饰器

1.1 原理

1.2 语法糖@

1.3 小结 

1.3.1 本小结解决

1.3.2 改进

2.无参数的函数装饰器增加自定义参数

2.2 不使用@

2.2 使用@

2.3 增加一般化参数(可变参数列表)

2.4 小结

3. 带参数的函数装饰器

3.1 存在的意义

3.2 如何编写

3.3 一般化的参数列表

3.4 小结

4. 复合函数装饰器调用

4.1 代码示例

4.2 小结

5. 解决函数装饰器的信息丢失

5.1 问题

5.2 解决方法

6. 类装饰器

6.1 一个简单例子

6.2 进阶参考


0.预备知识

0.1 函数指针

函数指针就是函数的名称,这个概念在C/C++/Python/JavaScript等语言中是通用的。

函数指针指向内存中函数的起始地址,函数指针后面加上()可以调用函数,并可以在()写入参数。

0.2 函数指针在python中的用法

python是一门脚本语言,函数指针可以作为参数传递,也可以以作为参数返回。例如:

# -*- coding:utf-8 -*-


def func_wrapper_1(func):
    print("我是函数包装器 1 , 我执行传入的函数并返回原函数的结果")
    return func()

def func_wrapper_2(func):
    print("我是函数包装器 2 , 我对传入的函数不做任何处理,返回传入的函数")
    return func

def hello(name="world"):
    print('hello, {}'.format(name))



def main():
    func_wrapper_1(hello)
    func_wrapper_2(hello)("小明")



if __name__ == "__main__":
    main()

[python3] python装饰器 简明教程_第1张图片 

解释:

def func_wrapper_1(func):
    print("我是函数包装器 1 , 我执行传入的函数并返回原函数的结果")
    return func()

第一个函数包装函数func_wrapper_1对传入的函数进行包装,并执行该函数,然后返回该函数的执行结果,这里由于func的函数仅仅是print(),返回值默认为None。

def func_wrapper_2(func):
    print("我是函数包装器 2 , 我对传入的函数不做任何处理,返回传入的函数")
    return func

 第二个函数func_wrapper_2对传入的函数进行包装,并将该函数本身传回,也不执行该函数。

def main():
    func_wrapper_1(hello)
    func_wrapper_2(hello)("小明")

在定义的主函数中,第一行代码:

func_wrapper_1(hello)

执行函数func_wrapper_1传入参数为函数指针hello,最终得到的返回值为None,即:

func_wrapper_1(hello)

的值为None。

第二行代码:

func_wrapper_2(hello)("小明")

func_wrapper_2传入的参数函数指针hello,返回依然是该函数指针,即:

func_wrapper_2(hello)

 的值为hello,之后再加上("小明")进行函数调用,等价于:

ret_func = func_wrapper_2(hello)
ret_func("小明")

也等价于:

hello("小明")

 

1.最简单的无参数的函数装饰器

1.1 原理

首先使用一个函数decorator_1来对一个待打包的函数func()进行打包,该函数decorator_1传入一个函数指针(在python中,函数指针与C/C++中的函数指针一样,都是函数的名称。调用函数的时候与C/C++也一样,使用"函数名称()"进行调用)

# -*- coding:utf-8 -*-


# 保持和原函数属性(默认参数)和返回值一致的装饰器
def decorator_1(func):
    def wrappered_func(str="world"):  # 返回的打包函数应该具有和被打包的原函数一致的默认参数,这样在无参数调用的时候,两者的行为才会一致
        print("{0} is doing something before function {1} calling".format("youheng", func.__name__))
        ret_value = func(str)
        print("{0} is doing something after function {1} calling".format("youheng", func.__name__))
        return ret_value  # 如果被包装的函数是下方的hello,这里的返回值应该是None

    return wrappered_func


def hello(name="world"):
    print('hello, {}'.format(name))


def main():
    print("无参数的python函数装饰器,不使用@写法")
    wrappered_func = decorator_1(hello)
    wrappered_func()  # 这里使用了默认参数
    print("-"*20)
    wrappered_func("奥里给")
    print("-"*20)
    decorator_1(hello)()  # 这里将函数的赋值和调用合为一步,使用了默认参数
    print("-"*20)
    decorator_1(hello)("奥里给") # 这里将函数的赋值和调用合为一步,使用参数"奥里给"


if __name__ == "__main__":
    main()

运行效果:

[python3] python装饰器 简明教程_第2张图片

1.2 语法糖@

接着使用一个符号@来获得一个打包的函数:

# -*- coding:utf-8 -*-


# 保持和原函数属性(默认参数)和返回值一致的装饰器
def decorator_1(func):
    def wrappered_func(str="world"):  # 返回的打包函数应该具有和被打包的原函数一致的默认参数,这样在无参数调用的时候,两者的行为才会一致
        print("{0} is doing something before function {1} calling".format("youheng", func.__name__))
        ret_value = func(str)
        print("{0} is doing something after function {1} calling".format("youheng", func.__name__))
        return ret_value

    return wrappered_func

@decorator_1
def hello(name="world"):
    print('hello, {}'.format(name))


def main():
    print("无参数的python函数装饰器,使用@写法")
    hello()
    print("-"*20)
    hello("奥里给")


if __name__ == "__main__":
    main()

输出:

[python3] python装饰器 简明教程_第3张图片

小结:装饰器@的就是python提供的一种“语法糖”,用于将一次函数的打包工作写成模板后通过:

@decorator_function_name
def a_func():
    pass


def main():
    a_func()


if __name__ == "__main__":
    main()

的类似形式进行调用。

1.3 小结 

1.3.1 本小结解决

通过1.2 和 1.3 的讨论,已经解决:

1. 如何构造一个装饰器函数decorator(),并保持默认参数和返回结果与被装饰函数行为相同

2. 如何使用@来对某个函数加上我们的decorator

3. 如何调用装饰后的函数

1.3.2 改进

包装后的函数确实保留了被包装函数的所有属性(默认参数)和行为。

但是,我们包装后的函数可能还需要一些自己的参数,来增加包装函数的多样性。

例如,思考这样一个问题,希望将下图中的youheng

[python3] python装饰器 简明教程_第4张图片

换为读者自己的id,该怎么操作?应该是要添加一个参数,下面介绍带参数的函数装饰器写法。

 

2.无参数的函数装饰器增加自定义参数

@decorator_1
def hello(name="world"):
    print('hello, {}'.format(name))

我们想要给这个装饰器加上一个参数user_name,并达到效果:

2.2 不使用@

还是回归本质,先不用语法糖:

# -*- coding:utf-8 -*-


# 保持和原函数属性(默认参数)和返回值一致的装饰器
def decorator_1(func):
    def wrappered_func(str="world", user_name="xiaoming"):  # 返回的打包函数应该具有和被打包的原函数一致的默认参数,这样在无参数调用的时候,两者的行为才会一致
        print("{0} is doing something before function {1} calling".format(user_name, func.__name__))
        ret_value = func(str)
        print("{0} is doing something after function {1} calling".format(user_name, func.__name__))
        return ret_value  # 如果被包装的函数是下方的hello,这里的返回值应该是None

    return wrappered_func


def hello(name="world"):
    print('hello, {}'.format(name))


def main():
    print("无参数的python函数装饰器,不使用@写法")

    decorator_1(hello)("奥里给")  # 这里将函数的赋值和调用合为一步,使用参数"奥里给"和默认参数"xiaoming"
    print("-"*20)
    decorator_1(hello)("奥里给", "lihua")  # 这里将函数的赋值和调用合为一步,使用参数"奥里给"和"lihua"


if __name__ == "__main__":
    main()

输出:

[python3] python装饰器 简明教程_第5张图片

这是很容易想到的,我们给返回的函数wrappered_func加上一个参数user_name就可以了。

 

2.2 使用@

如果要使用语法糖@,写法如下:

# -*- coding:utf-8 -*-


# 保持和原函数属性(默认参数)和返回值一致的装饰器
def decorator_1(func):
    def wrappered_func(str="world", user_name="xiaoming"):  # 返回的打包函数应该具有和被打包的原函数一致的默认参数,这样在无参数调用的时候,两者的行为才会一致
        print("{0} is doing something before function {1} calling".format(user_name, func.__name__))
        ret_value = func(str)
        print("{0} is doing something after function {1} calling".format(user_name, func.__name__))
        return ret_value  # 如果被包装的函数是下方的hello,这里的返回值应该是None

    return wrappered_func

@decorator_1
def hello(name="world"):
    print('hello, {}'.format(name))


def main():
    print("无参数的python函数装饰器,使用@写法")
    hello()
    print("-"*20)
    hello("lihua")


if __name__ == "__main__":
    main()

输出:

[python3] python装饰器 简明教程_第6张图片 

2.3 增加一般化参数(可变参数列表)

# -*- coding:utf-8 -*-


# 保持和原函数属性(默认参数)和返回值一致的装饰器
def decorator_1(func):
    def wrappered_func(str="world", user_name="xiaoming", *args, **kwargs ):  # 返回的打包函数应该具有和被打包的原函数一致的默认参数,这样在无参数调用的时候,两者的行为才会一致
        print("{0} is doing something before function {1} calling".format(user_name, func.__name__))
        print("args: {} kwargs: {} ".format(args, kwargs))
        ret_value = func(str)
        print("{0} is doing something after function {1} calling".format(user_name, func.__name__))
        return ret_value  # 如果被包装的函数是下方的hello,这里的返回值应该是None

    return wrappered_func

@decorator_1
def hello(name="world"):
    print('hello, {}'.format(name))


def main():
    print("无参数的python函数装饰器,使用@写法")
    hello(1, 2, 3, 4, color="red", length="10cm", height="10cm", depth="10cm")
    print("-"*20)
    hello("lihua")


if __name__ == "__main__":
    main()

输出:

[python3] python装饰器 简明教程_第7张图片 

这里要注意python中的参数的顺序:参考博客

2.4 小结

 给装饰器添加自己的参数也很容易,只需要在装饰器函数中的最终返回的那个函数的参数列表中添加自己的参数就可以了。

 

3. 带参数的函数装饰器

3.1 存在的意义

任何事物的出现都有其意义,带参数的函数装饰器,其出现让装饰函数时动态地提供参数成为可能。

虽然之前已经推广了无参数地函数装饰器理论上也能实现任意类型的传参,但是python作为一个高自由度的语言,其设计之初就为各种问题提供了多种解决方案,例如字符串可以使用r'str'\''str''\'str'。

同样的,python也支持给函数装饰器本身添加参数。

3.2 如何编写

1、2两个小节讨论了无参数函数装饰器,并推广到了一般情况,第3小结讨论给装饰器加上参数parameter,如:

@decorator_function_name(parameter)
def a_func():
    pass

如何写这样的一个decorator_function呢?

答:对之前的装饰器函数再做一层封装,并设置参数为parameter。

1、2节中的函数装饰器:


# 保持和原函数属性(默认参数)和返回值一致的装饰器
def decorator_1(func):
    def wrappered_func(str="world", user_name="xiaoming", *args, **kwargs ):  # 返回的打包函数应该具有和被打包的原函数一致的默认参数,这样在无参数调用的时候,两者的行为才会一致
        print("{0} is doing something before function {1} calling".format(user_name, func.__name__))
        print("args: {} kwargs: {} ".format(args, kwargs))
        ret_value = func(str)
        print("{0} is doing something after function {1} calling".format(user_name, func.__name__))
        return ret_value  # 如果被包装的函数是下方的hello,这里的返回值应该是None

    return wrappered_func

再做一层封装:

# -*- coding:utf-8 -*-


# 保持和原函数属性(默认参数)和返回值一致的装饰器
def decorator_with_para(parameter):
    def decorator_1(func):
        def wrappered_func(str="world", user_name="xiaoming", *args, **kwargs ):  # 返回的打包函数应该具有和被打包的原函数一致的默认参数,这样在无参数调用的时候,两者的行为才会一致
            print("{0} is doing something before function {1} calling".format(user_name, func.__name__))
            print("args: {} kwargs: {} ".format(args, kwargs))
            ret_value = func(str)
            print("{0} is doing something after function {1} calling".format(user_name, func.__name__))
            return ret_value  # 如果被包装的函数是下方的hello,这里的返回值应该是None
        # added logic here
        parameter()
        ###### added logic end here #####
        return wrappered_func
    return decorator_1  # 相当于传入了参数parameter,并返回原来的装饰器函数

def do_some_thing():
    print("I am doing somthing")

@decorator_with_para(do_some_thing)
def hello(name="world"):
    print('hello, {}'.format(name))


def main():
    print("带参数的python函数装饰器,使用@写法")
    hello(1, 2, 3, 4, color="red", length="10cm", height="10cm", depth="10cm")
    

if __name__ == "__main__":
    main()

输出:

[python3] python装饰器 简明教程_第8张图片

当然这个传入的参数并不是只有在上面标记的#add logic here 和 ###add logic end ###两者之间才能用,上面仅仅是示范,传入的参数parameter可以在 def decorator_with_para(parameter) 的函数体内的任意地方使用。

3.3 一般化的参数列表

带参数的函数装饰器的参数也可以一般化,即可以使用可变参数列表:

# -*- coding:utf-8 -*-


# 保持和原函数属性(默认参数)和返回值一致的装饰器
def decorator_with_para(*para_list, **para_dict):
    def decorator_1(func):
        def wrappered_func(str="world", user_name="xiaoming", *args, **kwargs):  # 返回的打包函数应该具有和被打包的原函数一致的默认参数,这样在无参数调用的时候,两者的行为才会一致
            print("{0} is doing something before function {1} calling".format(user_name, func.__name__))
            print("args: {} kwargs: {} ".format(args, kwargs))
            ret_value = func(str)
            print("{0} is doing something after function {1} calling".format(user_name, func.__name__))
            return ret_value  # 如果被包装的函数是下方的hello,这里的返回值应该是None
        # added logic here
        for each_func in para_list:
            each_func()
        ###### added logic end here #####
        return wrappered_func
    return decorator_1  # 相当于传入了参数parameter,并返回原来的装饰器函数

def eat():
    print("I am eating")

def code():
    print("I am coding")

def exercise():
    print("I am jogging")

def sleep():
    print("I am sleeping")

@decorator_with_para(eat, code, exercise, sleep)
def hello(name="world"):
    print('hello, {}'.format(name))


def main():
    print("带参数的python函数装饰器,使用@写法")
    hello(1, 2, 3, 4, color="red", length="10cm", height="10cm", depth="10cm")


if __name__ == "__main__":
    main()

输出:

[python3] python装饰器 简明教程_第9张图片 

3.4 小结

 带参数的函数装饰器的使用很简单,就是在原来不带参数的函数装饰器外面再包上一层函数,传入参数供内部所有函数层的函数使用,并最终再最外层返回原来的函数装饰器即可。

 

4. 复合函数装饰器调用

4.1 代码示例

# -*- coding:utf-8 -*-

def eat(func):
    def wrapper_func():
        print("I am eating")
        return func()
    return wrapper_func

def code(func):
    def wrapper_func():
        print("I am coding")
        return func()
    return wrapper_func


def exercise(func):
    def wrapper_func():
        print("I am jogging")
        return func()
    return wrapper_func


def sleep(func):
    def wrapper_func():
        print("I am sleeping")
        return func()
    return wrapper_func


@eat
@code
@exercise
@sleep
def hello():
    print("That's how I spend my day, lol")


def main():
    print("复合装饰器调用")
    hello()


if __name__ == "__main__":
    main()

输出:

[python3] python装饰器 简明教程_第10张图片

4.2 小结

执行规律为:先加上的后执行。

 

5. 解决函数装饰器的信息丢失

5.1 问题

如果尝试在函数修饰器中输出函数的元信息(函数名、函数的doc文档等):

# -*- coding:utf-8 -*-

def eat(func):
    def wrapper_func():
        print("func name = {}".format(func.__name__))
        print("I am eating")
        return func()
    return wrapper_func

def code(func):
    def wrapper_func():
        print("func name = {}".format(func.__name__))
        print("I am coding")
        return func()
    return wrapper_func


def exercise(func):
    def wrapper_func():
        print("func name = {}".format(func.__name__))
        print("I am jogging")
        return func()
    return wrapper_func


def sleep(func):
    def wrapper_func():
        print("func name = {}".format(func.__name__))
        print("I am sleeping")
        return func()
    return wrapper_func


@eat
@code
@exercise
@sleep
def hello():
    print("That's how I spend my day, lol")


def main():
    print("复合装饰器调用")
    hello()


if __name__ == "__main__":
    main()

输出:

[python3] python装饰器 简明教程_第11张图片

本来应该输出func_name = hello

5.2 解决方法

使用python内置的模块:

from functools import wraps

然后再要返回的wrapper函数上调用这个装饰器,传入参数为函数的名称:

# -*- coding:utf-8 -*-

from functools import wraps

def eat(func):
    @wraps(func)
    def wrapper_func():
        print("func name = {}".format(func.__name__))
        print("I am eating")
        return func()
    return wrapper_func

def code(func):
    @wraps(func)
    def wrapper_func():
        print("func name = {}".format(func.__name__))
        print("I am coding")
        return func()
    return wrapper_func


def exercise(func):
    @wraps(func)
    def wrapper_func():
        print("func name = {}".format(func.__name__))
        print("I am jogging")
        return func()
    return wrapper_func


def sleep(func):
    @wraps(func)
    def wrapper_func():
        print("func name = {}".format(func.__name__))
        print("I am sleeping")
        return func()
    return wrapper_func


@eat
@code
@exercise
@sleep
def hello():
    print("That's how I spend my day, lol")


def main():
    print("复合装饰器调用")
    hello()


if __name__ == "__main__":
    main()

 输出:

[python3] python装饰器 简明教程_第12张图片

6. 类装饰器

前面的0-5共6个小结讨论了python函数装饰器的所有内容,最后讨论python的类装饰器。

这部分内容可以进一步拓展,这里仅作简单讨论:

6.1 一个简单例子

# -*- coding:utf-8 -*-

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

    def __call__(self):
        print('class decorator logic before calling func')
        self._func()
        print('class decorator logic after calling func')

@class_name
def a_func():
    print ('a_func working')

def main():
    a_func()

if __name__ == "__main__":
    main()

输出:

 [python3] python装饰器 简明教程_第13张图片

6.2 进阶参考

可以进一步学习:参考博客

 

 

你可能感兴趣的:(Python)