【Python 语法进阶】Python 装饰器入门与简单应用

一、装饰器概述

在Python中,装饰器是一种设计模式,装饰器可以在不改变被装饰对象(函数、类)的同时为对象增添新的功能。这也被成为元编程(metaprogramming),因为一部分代码尝试在编译时修改另一部分代码。

二、装饰器理解必备知识

1. 必备一:Python 中一切皆对象

在 Python 中,一切皆对象,包括函数、类等,而变量名只是用于指向对象的标识符,多个不同的变量可以指向同一个对象。如下面代码表明两个变量名指向了同一个函数对象,则以两个变量名加上 () 可以调用同一个函数。

def plus_one(number):
    return number + 1


add_one = plus_one
print("add_one(5) = %d" % add_one(5))

代码运行结果为:

add_one(5) = 6

2. 必备二:函数可作为参数传递

在 Python 中,指向函数的变量可以作为参数传递给另外一个函数,如下述代码所述:

def plus_one(number):
    return number + 1


def function_call(function):
    number_to_add = 5
    return function(number_to_add)


print("function_call(plus_one) = %d" % function_call(plus_one))

代码运行结果为:

function_call(plus_one) = 6

3. 必备三:Python 的闭包特性

从函数的嵌套定义到 Python 闭包中,还有如下结论:

  • 指向函数对象的变量(即函数名)可被当作返回值返回;
  • 内层嵌套函数可以访问并记录外层函数中定义的变量。

4. 必备四:Python 可调用对象本质

在 Python 中,函数和方法都被称作可调用对象。实际上,Python 中任何实现了魔法方法 __call__ 的对象都是可调用对象。因此,Python 中,装饰器就是一个可调用对象,该可调用对象能够返回一个可调用对象。

实际上,可以通过 函数名_.__dir__ 查看函数对象的确有魔法方法 __call__

三、装饰器初步探究

在 Python 中,装饰器可以是一个函数也可以是一个类,被装饰的对象也既可以是一个函数或者一个类。其中最简单的一种模式为:装饰器和被装饰对象都是一个函数,本文将主要围绕这类装饰器进行介绍,关于其他更高级的装饰器,如:装饰器和/或被装饰对象为类,请见Python装饰器进阶与高级应用

1. 装饰无参数无返回值函数

基于上述必备知识,先看下列代码示例:

def make_pretty(func):
    def inner():
        print("I got decorated")
        func()

    return inner


def ordinary():
    print("I am ordinary")


def main():
    ordinary()

    pretty = make_pretty(ordinary)
    pretty()


if __name__ == '__main__':
    main()

上述代码的运行结果为:

I am ordinary
I got decorated
I am ordinary

上述示例代码中,make_pretty 就是一个装饰器,在下述步骤中:

pretty = make_pretty(ordinary)

函数 ordinary 被装饰,且装饰器的返回值被赋给了变量 pretty 。因此,装饰器函数在原函数的基础上增添了新的功能。

事实上,基于必备一,通常在装饰一个函数后,用以接收装饰器返回值的变量名和被装饰函数名保持一致,用上述例子,即:

ordinary = make_pretty(ordinary)

因此,Python 中对此有如下简化语法(在 Python,这叫所谓的语法糖):

@make_pretty
def ordinary():
    print("I am ordinary")

即上述语法等价于:

def ordinary():
    print("I am ordinary")
ordinary = make_pretty(ordinary)

2. 装饰有参数无返回值函数

上述装饰器非常简单且且只能装饰不带任何参数的函数。如果希望装饰如下所示函数:

def divide(a, b):
    return a/b

由于上述函数接受两个参数,且当传递b等于零时会发生异常,下面通过非捕获异常的方式完善上述代码:

def smart_divide(func):
    print("Preparing to decorate the divide func")

    def inner(a, b):
        print("I am going to divide", a, "and", b)
        if b == 0:
            print("Whoops! cannot divide because divisor is zero...")
            return

        func(a, b)

    return inner


@smart_divide
def divide(a, b):
    print("a / b = ", a / b)


def main():
    print("-" * 25)
    divide(2, 5)
    divide(2, 0)


if __name__ == '__main__':
    main()


上述代码的运行结果为:

Preparing to decorate the divide func
-------------------------
I am going to divide 2 and 5
a / b = 0.4
I am going to divide 2 and 0
Whoops! cannot divide because divisor is zero…

即上述代码完成了对于接收两个参数的函数进行装饰。事实上,由于:

@smart_divide
def divide(a, b):
    print("a / b = ", a / b)

等价于:

def divide(a, b):
    print("a / b = ", a / b)
divide = smart_divide(divide)

即此时变量dividesmart_divide的返回值inner同时指向嵌套函数处,则在第22、23行时,参数ab相当于分别被传递至嵌套函数inner处。进而,在第10行调用func指向的原函数时,参数ab分别被进一步传递。

3. 装饰有参数有返回值函数

进一步地,如果上述被装饰函数divide()有返回值,则对其进行装饰的代码如下:

def smart_divide(func):
    print("Preparing to decorate the divide func")

    def inner(a, b):
        print("I am going to divide", a, "and", b)
        if b == 0:
            print("Whoops! cannot divide because divisor is zero...")
            return

        return func(a, b)

    return inner


@smart_divide
def divide(a, b):
    return a / b


def main():
    print("-" * 25)
    
    ret1 = divide(2, 5)
    print(ret1)

	ret2 = divide(2, 0)
    print(ret2)


if __name__ == '__main__':
    main()

实际上,由于程序第23、26行调用被装饰后的divide()函数相当于调用inner函数,在调用inner()时,由于需要使用func调用指向被装饰前的divide()函数并确保后者仍旧正确返回,则需要在inner()中返回func()的返回值。

4. 装饰接受任意参数的函数

实际上,为了使得装饰器可以通用,需要考虑Python中函数接收不定长参数的特性,为了确保装饰器能够较为通用,即对接收不定长(元组、字典)参数的函数进行装饰,则通用装饰器有如下格式:

def universal_decorator(func):
    def inner(*args, **kwargs):
        print("Decorative operations for func")
        return func(*args, **kwargs)
    return inner
    

需要注意的是,嵌套函数参数位置的argskwargs分别表示元组和字典,而在嵌套函数中调用被装饰前函数时,使用的*args**kwargs分别表示对元组和字典先进行拆包,然后再传递。

四、装饰器的部分简单应用

上面是对Python中装饰器进行的初步探究,下面是装饰器在实际程序中的部分简单应用。

首先,基于对上述讨论的总结,下面先给出用于介绍装饰器实际应用时用到的装饰器模板,后续更加复杂的装饰器也将基于该模板来修改。

import functools

def decorator(func):
    @functools.wraps(func)
    def wrapper_decorator(*args, **kwargs):
        # Do something before
        value = func(*args, **kwargs)
        # Do something after
        return value
    return wrapper_decorator

关于程序中为何使用functools模块内的wrap()函数,请见:Python模块functools学习笔记

1. 程序计时

下面的@timer装饰器可以测量一个函数执行所耗费的时间,并将该时间打印至控制台。

import functools
import time


def timer(func):
    """Print the runtime of the decorated function"""

    @functools.wraps(func)
    def wrapper_timer(*args, **kwargs):
        start_time = time.perf_counter()
        value = func(*args, **kwargs)
        end_time = time.perf_counter()
        run_time = end_time - start_time
        print(f"Finished {func.__name__!r} in {run_time:.4f} secs")
        return value

    return wrapper_timer


@timer
def waste_some_time(num_times):
    for _ in range(num_times):
        sum([i ** 2 for i in range(10000)])


waste_some_time(10)

需要注意的是:如果你只是希望了解自己的程序运行大概会耗费多长时间,那么自定的@timer装饰器已经足够。如果你希望对程序运行时间做更精确的度量,你需要考虑使用Python标准库中的timeit模块。该模块会暂时性关闭Python解释器的垃圾回收以多次运行待测量代码以剔除噪声。

2. 调试代码

下面的@debug装饰器会在被装饰函数每次执行时,打印函数的参数以及返回值:

import functools


def debug(func):
    """Print the function signature and return value"""

    @functools.wraps(func)
    def wrapper_debug(*args, **kwargs):
        args_repr = [repr(a) for a in args]  # 1
        kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]  # 2
        signature = ", ".join(args_repr + kwargs_repr)  # 3
        print(f"Calling {func.__name__}({signature})")
        value = func(*args, **kwargs)
        print(f"{func.__name__!r} returned {value!r}")  # 4
        return value

    return wrapper_debug


@debug
def make_greeting(name, age=None):
    if age is None:
        return f"Howdy {name}!"
    else:
        return f"Whoa {name}! {age} already, you are growing up!"


print(make_greeting("Benjamin"), end="\n" * 2)
print(make_greeting("Richard", age=112), end="\n" * 2)
print(make_greeting(name="Dorrisile", age=116), end="\n" * 2)

上述代码的执行结果为:

Calling make_greeting(‘Benjamin’)
‘make_greeting’ returned ‘Howdy Benjamin!’
Howdy Benjamin!


Calling make_greeting(‘Richard’, age=112)
‘make_greeting’ returned ‘Whoa Richard! 112 already, you are growing up!’
Whoa Richard! 112 already, you are growing up!


Calling make_greeting(name=‘Dorrisile’, age=116)
‘make_greeting’ returned ‘Whoa Dorrisile! 116 already, you are growing up!’
Whoa Dorrisile! 116 already, you are growing up!

下面简单分析上述实现@debug装饰器代码原理和涉及的Python语法点,上述代码中的注释编号对应于如下有序列表的编号:

  1. 创建一个由位置参数组成的列表。使用repr()函数以一种无歧义的方式获得代表每个参数的字符串表现形式。
  2. 创建一个由关键字参数组成的列表。其中f-string用来以key=value的形式格式化字符串。
  3. 位置参数和关键字参数由逗号拼接成一个签名。
  4. 返回值在函数被执行后得到打印。

上述@debug装饰器还能用于Python中标准库函数,如下列代码用于计算数学中的常量 e e e的值,其计算公式为:

e = ∑ n = 0 ∞ 1 n ! = 1 0 ! + 1 1 ! + 1 2 ! + ⋅ ⋅ ⋅ = 1 1 + 1 1 + 1 1 × 2 + ⋅ ⋅ ⋅ e={\sum_{n=0}^\infty\frac{1}{n!}}=\frac{1}{0!}+\frac{1}{1!}+\frac{1}{2!}+\cdot\cdot\cdot=\frac{1}{1}+\frac{1}{1}+\frac{1}{1\times2}+\cdot\cdot\cdot e=n=0n!1=0!1+1!1+2!1+=11+11+1×21+

import math
from decorators import debug

# 应用自定义装饰器于Python标准库函数
math.factorial = debug(math.factorial)


def approximate_e(terms=18):
    return sum(1 / math.factorial(each) for each in range(terms))


def main():
    print(approximate_e(terms=5))


if __name__ == '__main__':
    main()

上述代码的运行结果为:

Calling factorial(0)
‘factorial’ returned 1
Calling factorial(1)
‘factorial’ returned 1
Calling factorial(2)
‘factorial’ returned 2
Calling factorial(3)
‘factorial’ returned 6
Calling factorial(4)
‘factorial’ returned 24
2.708333333333333

需要说明的是,为了代码的解耦合与重用性,上面代码将装饰器函数都放在了文件decorators.py中,在其他.py文件中使用哪一个装饰器就导入对应的函数。

3. 代码减速

乍一看起来很奇怪,因为一般都希望代码的运行速度可以越快越好,但在一种情况下可能的确是必要的,如:一段代码需要连续检查网页资源是否已经更新。下面的例子可以实现调用代码前延时一秒钟:

import functools
import time


def slow_down(func):
    """调用函数之前延时一秒钟"""

    @functools.wraps(func)
    def wrapper_slow_down(*args, **kwargs):
        time.sleep(1)
        return func(*args, **kwargs)

    return wrapper_slow_down


@slow_down
def countdown(from_number):
    if from_number < 1:
        print("发射!")
    else:
        print(from_number)
        countdown(from_number - 1)


def main():
    countdown(from_number=5)


if __name__ == '__main__':
    main()

上述代码的运行结果为:

5
4
3
2
1
发射!

4. 注册插件

事实上,装饰器也一定要改变被装饰函数的行为,装饰器也可以仅仅注册一下一个函数,这一特点可以用于创建一个轻量级的插件架构,如下列示例代码:

import random
import functools

PLUGINS = dict()


def register(func):
    """以插件形式注册一个函数"""
    PLUGINS[func.__name__] = func

    @functools.wraps(func)  # 1
    def wrap(name):  # 2
        return func(name)  # 3

    return wrap  # 4


@register
def say_hello(name):
    return f"Hello {name}"


@register
def be_awesome(name):
    return f"Yo {name}, together we are the awesomest!"


def randomly_greet(name):
    greeter, greeter_func = random.choice(list(PLUGINS.items()))
    print(f"Using {greeter!r}")
    return greeter_func(name)


def main():
    print(PLUGINS)
    print(randomly_greet("Eric Idle"))


if __name__ == '__main__':
    main()

上述代码的运行结果为:

{‘say_hello’: <function say_hello at 0x7fc47a282ea0>, ‘be_awesome’: <function be_awesome at 0x7fc47a282f28>}
Using ‘say_hello’
Hello Eric Idle

上述@register装饰器仅在全局字典变量PLUGINS中存储被装饰函数的引用。需要注意的是:代码中位置#1、#2、#3、#4处的代码均可省略,外部函数作用域内且内层函数作用域外之间的代码会在执行@register就被执行。

5. 用户登录

五、参考资料

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值