Python 全栈 60 天精通之路

Day 26:Python 装饰器的本质解密,结合 3 个装饰器的案例

发布日期:2022年3月12日 18:02 阅读: 206 访问: 206

装饰器应用广泛Python 中使用 @函数名字,放在某些函数上面,起到增强它们功能的作用。很多朋友觉得使用装饰器太魔幻,始终不知道怎么使用。装饰器带来的好处也显而易见,它会使得代码更加简洁,代码的复用性大大提升。因此,装饰器被广泛使用。Python 中到处都能看到装饰器的身影。不光在 Python,其他语言

装饰器应用广泛

Python 中使用 @函数名字,放在某些函数上面,起到增强它们功能的作用。

很多朋友觉得使用装饰器太魔幻,始终不知道怎么使用。装饰器带来的好处也显而易见,它会使得代码更加简洁,代码的复用性大大提升。

因此,装饰器被广泛使用。Python 中到处都能看到装饰器的身影。

不光在 Python,其他语言如 Java 中,装饰器被称作注解,也被应用广泛。

Python 前端最流行的框架之一 Flask,URL 地址和 controller 层的映射处理函数,就是使用装饰器。

import Flask,同时创建一个 app:

from flask import Flask

app = Flask(__name__)

URL 路径 / 与处理函数建立映射关系,正是通过装饰器 app.route 控制层处理响应前端,并返回字符串 hello world 到前端界面。

@app.route('/')
def index():
    return "hello world"

Python 支持异步编程,从中也能看到装饰器的身影。

下面例子,使用装饰器 @asyncio.coroutine,将一个生成器asyncio_open_conn 标记为 coroutine 类型。

import asyncio

@asyncio.coroutine
def asyncio_open_conn(host:str):
    print("using asyncio builds web connection")
    connect = yield from asyncio.open_connection(host, 80)
    print("get connect of %s"%(host,))

loop = asyncio.get_event_loop()
connections = [asyncio_open_conn(host) for host in [
    'www.sina.com', 'www.baidu.com']]
loop.run_until_complete(asyncio.wait(connections))
loop.close()

异步请求建立 'www.sina.com'、'www.baidu.com' 两个连接,打印结果:

using asyncio builds web connection
using asyncio builds web connection
get connect of www.baidu.com
get connect of www.sina.com

装饰器还在日志管理、图像绘图等有重要的应用。因此,我们很有必要掌握装饰器,逐渐能做到灵活使用。

那么,今天我们就一步一步来理解装饰器的本质。我会尽量用最通俗的语言,通过代码和案例来辅助大家理解。

call_print 装饰器

为了帮助大家更容易理解装饰器,以下函数包装,可能会让某些函数丢掉一些函数属性等信息。

记住,我们的主要目标:理解装饰器。

首先,定义函数 call_print,它的入参 f 为一个函数,它里面内嵌一个函数 g,并返回函数 g:

def call_print(f):
    def g():
        print('you\'re calling %s function' % (f.__name__,))
    return g

Python 中,@call_print 函数,放在函数上面,函数 call_print 就变为装饰器。

变为装饰器后,我们不必自己去调用函数 call_print。

@call_print
def myfun():
    pass


@call_print
def myfun2():
    pass

直接调用被装饰的函数,就能调用到 call_print,观察输出结果:

In [27]: myfun()
you're calling myfun function

In [28]: myfun2()
you're calling myfun2 function

使用 call_print,@call_print 放置在任何一个新定义的函数上面。都会默认输出一行,输出信息:正在调用这个函数的名称。

有些朋友一定关心,这是怎么做到的。我们明明没有调用 call_print,但是却能输出实现它的功能?

下面,就来回答这个问题。

装饰器本质

call_print 装饰器实现效果,与下面调用方式,实现的效果是等效的。

def myfun():
    pass


def myfun2():
    pass


def call_print(f):
    def g():
        print('you\'re calling %s function' % (f.__name__,))
    return g

下面两行代码,对于理解装饰器,非常代码:

myfun = call_print(myfun)

myfun2 = call_print(myfun2)

call_print(myfun) 后不是返回一个函数吗,然后,我们再赋值给被传入的函数 myfun。

也就是 myfun 指向了被包装后的函数 g,并移花接木到函数 g,使得 myfun 额外具备了函数 g 的一切功能,变得更强大。

以上就是装饰器的本质。

再次调用 myfun、myfun2 时,与使用装饰器的效果完全一致。

In [32]: myfun()
you're calling myfun function

In [33]: myfun2()
you're calling myfun2 function

wraps 装饰器

在使用上面装饰器 call_print 后,

def call_print(f):
    def g():
        print('you\'re calling %s function' % (f.__name__,))
    return g

@call_print
def myfun():
    pass

@call_print
def myfun2():
    pass

我们打印 myfun:

myfun()

myfun2()

print(myfun)

打印结果:

you're calling myfun function
you're calling myfun2 function
<function call_print.<locals>.g at 0x00000215D90679D8>

发现,被装饰的函数 myfun 名字竟然变为 g,也就是定义装饰器 call_print 时使用的内嵌函数 g。

Python 的 functools 模块,wraps 函数能解决这个问题。

解决方法,如下,只需重新定义 call_print,并在 g 内嵌函数上,使用 wraps 装饰器。

from functools import wraps

def call_print(f):
    @wraps(f)
    def g():
        print('you\'re calling %s function' % (f.__name__,))
    return g

当再次调用打印 myfun 时,函数 myfun 打印信息正常:

print(myfun)
# 结果
<function myfun at 0x000002A34A8479D8>

案例 1:异常次数

这个装饰器,能统计出某个异常重复出现到指定次数时,历经的时长。

import time
import math

def excepter(f):
    i = 0
    t1 = time.time()
    def wrapper():
        try:
            f()
        except Exception as e:
            nonlocal i
            i += 1
            print(f'{e.args[0]}: {i}')
            t2 = time.time()
            if i == n:
                print(f'spending time:{round(t2-t1,2)}')
    return wrapper

关键词 nonlocal 在前面我们讲到了,今天就是它的一个应用。

它声明变量 i 为非局部变量;如果不声明,根据 Python 的 LEGB 变量搜寻规则(这个规则我们在前面的专栏中也讲到过),i+=1 表明 i 为函数 wrapper 内的局部变量,因为在 i+=1 引用(reference)时,i 未被声明,所以会报 unreferenced variable 的错误。

使用创建的装饰函数 excepter,n 是异常出现的次数。

共测试了两类常见的异常:被零除和数组越界。

n = 10# except count

@excepter
def divide_zero_except():
    time.sleep(0.1)
    j = 1/(40-20*2)

# test zero divived except
for _ in range(n):
    divide_zero_except()


@excepter
def outof_range_except():
    a = [1,3,5]
    time.sleep(0.1)
    print(a[3])

# test out of range except
for _ in range(n):
    outof_range_except()

打印出来的结果如下:

division by zero: 1
division by zero: 2
division by zero: 3
division by zero: 4
division by zero: 5
division by zero: 6
division by zero: 7
division by zero: 8
division by zero: 9
division by zero: 10
spending time:1.01
list index out of range: 1
list index out of range: 2
list index out of range: 3
list index out of range: 4
list index out of range: 5
list index out of range: 6
list index out of range: 7
list index out of range: 8
list index out of range: 9
list index out of range: 10
spending time:1.01

案例 2:绘图

导入本次实验所用的 4 种常见分布,连续分布的代表:beta 分布、正态分布、均匀分布,离散分布的代表:二项分布。

import numpy as np
from scipy.stats import beta, norm, uniform, binom
import matplotlib.pyplot as plt
from functools import wraps

定义带参数的装饰器

绘图装饰器带有四个参数分别表示,legend 的两类说明文字,y 轴 label,保存的 PNG 文件名称。

注意使用 wraps,包装了内嵌函数 myplot。这样做的好处,被包装的函数名字不会被改变。

# 定义带四个参数的画图装饰器
def my_plot(label0=None, label1=None, ylabel='probability density function', fn=None):
    def decorate(f):
        @wraps(f)
        def myplot():
            fig = plt.figure(figsize=(16, 9))
            ax = fig.add_subplot(111)
            x, y, y1 = f()
            ax.plot(x, y, linewidth=2, c='r', label=label0)
            ax.plot(x, y1, linewidth=2, c='b', label=label1)
            ax.legend()
            plt.ylabel(ylabel)
            # plt.show()
            plt.savefig('./img/%s' % (fn,))
            plt.close()
        return myplot
    return decorate

均匀分布

从图中可看出,红色概率密度函数只在 0~1 才会发生,曲线与 x 轴的 0~1 区间所封闭的面积为全概率 1.0。

@my_plot(label0='b-a=1.0', label1='b-a=2.0', fn='uniform.png')
def unif():
    x = np.arange(-0.01, 2.01, 0.01)
    y = uniform.pdf(x, loc=0.0, scale=1.0)
    y1 = uniform.pdf(x, loc=0.0, scale=2.0)
    return x, y, y1

二项分布

红色曲线表示发生一次概率为 0.3,重复 50 次的密度函数,二项分布期望值为 0.3*50=15 次。看到这 50 次实验,很可能出现的次数为 10~20。可与蓝色曲线对比分析。

@my_plot(label0='n=50,p=0.3', label1='n=50,p=0.7', fn='binom.png', ylabel='probability mass function')
def bino():
    x = np.arange(50)
    n, p, p1 = 50, 0.3, 0.7
    y = binom.pmf(x, n=n, p=p)
    y1 = binom.pmf(x, n=n, p=p1)
    return x, y, y1

高斯分布

红色曲线表示均值为 0,标准差为 1.0 的概率密度函数,蓝色曲线的标准差更大,所以它更矮胖,显示出取值的多样性,和不稳定性。

# 高斯分布
@my_plot(label0='u=0.,sigma=1.0', label1='u=0.,sigma=2.0', fn='guass.png')
def guass():
    x = np.arange(-5, 5, 0.1)
    y = norm.pdf(x, loc=0.0, scale=1.0)
    y1 = norm.pdf(x, loc=0., scale=2.0)
    return x, y, y1

beta 分布

beta 分布的期望值如下,可从下面的两条曲线中加以验证:

@my_plot(label0='a=10., b=30.', label1='a=4., b=4.', fn='beta.png')
def bet():
    x = np.arange(-0.1, 1, 0.001)
    y = beta.pdf(x, a=10., b=30.)
    y1 = beta.pdf(x, a=4., b=4.)
    return x, y, y1

统一调用以上四个函数,分别绘制概率曲线:

distrs = [unif, bino, guass, bet]
for distri in distrs:
    distri()

小结

今天与大家一起讨论了应用广泛的装饰器:

  • 使用自定义的 call_print 装饰器
  • 讨论装饰器的本质:myfun 指向被包装后的函数 g,并移花接木到函数 g
  • 使用 wraps 装饰器,解决被装饰函数属性信息被篡改的问题
  • 两个实际应用装饰器的案例