Python教程

一、 必要知识

在开始说装饰器之前,需要大家熟悉之前说过的相关知识点:

  • 函数即“变量”: 函数名就是一个变量名,它的值就是其对应的函数体;函数体也可以赋值给其它变量,通过这个变量也能调用函数;
  • 嵌套函数: 函数内部可以嵌套定义(一层或多层)函数,内部函数可以在函数体内部调用,也可以当做返回值返回;
  • 闭包: 在一个嵌套函数中,内部函数可以调用外部非全局变量并且不受外部函数生命周期的影响;
  • 高阶函数: 函数的参数可以是函数;

简单来讲,装饰器就是对这些内容的整合和经典应用。如果不了解这些内容,可以查看 这篇文章。

二、情景模拟

我们将通过对一个功能需求的分析和解决过程来探究一下“装饰器是什么”以及“装饰器的一些特性”。

1. 场景说明

假设我现在已经定义了一些函数,并且这些函数都已经被线上业务广泛应用。

import time

def func1():
    time.sleep(1)
    print('func1')
    return 'func1'

def func2():
    time.sleep(2)
    print('func2')
    return 'func2'

2. 功能需求

现在线上某个业务响应时间过长,需要在不影响线上服务的情况下分别统计这些函数的运行时间来定位故障。

需求解读:

既然不能影响线上业务,那么必然是不能要求函数调用方去更改代码的,这当然包括调用方式。

实现方式:

  • 为每个函数单独添加重复的代码
  • 把统计运行时间的的代码封装成一个可接收函数作为参数的高阶函数
  • 使用嵌套函数改进上面的高阶函数

3. 实现方式及改进过程

初步实现:分别为各个函数添加运行时间统计功能

import time
import sys

def func1():
    start_time = time.time()
    time.sleep(1)
    print('func1')
    end_time = time.time()
    func_name = sys._getframe().f_code.co_name
    print('%s run time is: %s' % (func_name, (end_time - start_time)))
    return 'func1'

def func2():
    start_time = time.time()
    time.sleep(2)
    print('func2')
    end_time = time.time()
    func_name = sys._getframe().f_code.co_name
    print('%s run time is: %s' % (func_name, (end_time - start_time)))
    return 'func2'

函数调用:

func1()
func2()

输出结果:

func1
func1 run time is: 1.000861644744873
func2
func2 run time is: 2.0005600452423096

存在的问题:

  • 功能是实现了,但是存在以下几个问题:
  • 如果涉及的函数太多,那么需要做大量的重复性工作;
  • 如果这些函数分散在不同的模块,且有不同的人维护,那么需要进行协商沟通来保证代码功能一致性;
  • 故障解决后,还需要一个一个的去删除或注释调试代码,又是一次大量的重复性工作;
  • 如果其他业务也遇到相同问题,需要再次在多个地方复写这些代码。

改进思路:

要避免重复劳动,提高代码重用性,大家很自然就会想到把这个功能封装成一个函数。

改进1:自定义一个(高阶)函数来提高统计代码复用性

import time

def func1():
    time.sleep(1)
    print('func1')
    return 'func1'

def func2():
    time.sleep(2)
    print('func2')
    return 'func2'

def print_run_time(f):
    time_start = time.time()
    ret = f()
    time_end = time.time()
    func_name = f.__name__
    print('%s run time is: %s' % (func_name, (time_end - time_start)))
    return ret

函数调用:

print_run_time(func1)
print_run_time(func2)

输出结果:

func1
func1 run time is: 1.0003290176391602
func2
func2 run time is: 2.0004265308380127

存在的问题:

统计代码的重复性工作解决了,但是新的问题出现了:此时只能通过print_run_time(f)函数去调用原来的函数了(func1, func2, ...),这显然已经改变了函数的调用方式,因此是不合理的。

看上去越改越差劲了,我们继续分析下,会峰回路转的。

改进思路:

如果函数调用方式不能修改,那么我们只能给原来的函数名重新赋值一个新的函数体了,这个新的函数体就应该是添加完统计功能之后的函数体。我们看看下面这两种实现行不行:

func1 = print_run_time
func2 = print_run_time

print_run_time(f)是有参数的,而原函数func1和func2是没参数的,调用方式还是发生改变了,因此这种方式不可行。

func1 = print_run_time(func1)
func2 = print_run_time(func2)

上面这种方式显然更不行了,因为print_run_time(f)返回的是原函数的返回值,而这个返回值不是一个函数,这将导致被重新赋值后的func1和func2无法被调用。

那么我们是否可以把print_run_time(f)函数的函数体定义为一个内部的嵌套函数,然后将这个内部的嵌套函数作为print_run_time(f)函数的返回值呢?这貌似是说的通的,看下面的实现。

改进2:使用嵌套函数和闭包改进上面定义的高阶函数

import time

def func1():
    time.sleep(1)
    print('func1')
    retrun 'func1'

def func2():
    time.sleep(2)
    print('func2')
    return 'func2'

def print_run_time(f):
    def inner():
        time_start = time.time()
        ret = f()
        time_end = time.time()
        func_name = f.__name__
        print('%s run time is: %s' % (func_name, (time_end - time_start)))
        return ret
    return inner
    
func1 = print_run_time(func1)
func2 = print_run_time(func2)

函数调用:

func1()
func2()

输出结果:

func1
func1 run time is: 1.0003256797790527
func2
func2 run time is: 2.000091791152954

Cool! We got it! 我们现在所需要的做的是在所有需要统计运行时长的函数定义之后的任意地方执行一下下面这条语句就可以了:

funcN = print_run_time(funcN)

存在的问题:

我们貌似忽略了一个问题,如果原函数有参数怎么办?

改进思路:

是的,我们定义print_run_time(f)函数的内部函数inner()时,为它定义相应的参数就可以了。由于每个函数的参数数量是不同的,因此inner函数的参数应该是可变(长)参数。

改进3:支持原函数传递参数

def func1(string):
    print(string)
    time.sleep(1)
    print('func1')
    return 'func1 return'

def func2():
    time.sleep(2)
    print('func2')
    
def print_run_time(f):
    def inner(*args, **kwargs):
        time_start = time.time()
        ret = f(*args, **kwargs)
        time_end = time.time()
        func_name = f.__name__
        print('%s run time is: %s' % (func_name, (time_end - time_start)))
        return ret
    return inner

func1 = print_run_time(func1)
func2 = print_run_time(func2)

函数调用:

ret1 = func1('decorator test!')
print(ret1)
ret2 = func2()
print(ret2)

输出结果:

decorator test!
func1
func1 run time is: 1.0001435279846191
func1 return
func2
func2 run time is: 2.0005276203155518
None

到目前为止:统计函数运行时间的功能实现了,函数原来的调用方式没有发生改变,原函数的定义也没有发生改变。其实这就是是装饰器的雏形。

推荐学习Python教程运维必备Python基础语法全讲解

继续浏览:《Python装饰器教程(二)