头图

本文首发于公众号:Hunter后端

原文链接:Python笔记三之闭包与装饰器

这一篇笔记介绍 Python 里面的装饰器。

在介绍装饰器前,首先提出这样一个需求,我想统计某个函数的执行时间,假设这个函数如下:

import time

def add(x, y):
    time.sleep(1)
    return x + y

想要统计 add 函数的执行时间,可以如何操作,在一般情况下,可能会想到如下操作:

start_time = time.time()
add(1, 2)
end_time = time.time()
print("函数执行时间为:", end_time - start_time)

而如果我们想要统计很多个函数的执行时间,然后打印出来,应该如何操作呢?

这里就可以用上 Python 里装饰器的操作。

本篇笔记目录如下:

  1. 闭包

    1. 闭包
    2. 闭包实现计数器

      1. 自由变量
  2. 装饰器
  3. 装饰器代码示例装饰器原理
  4. 装饰器加参数
  5. 多重装饰器
  6. 装饰器类

1、闭包

在介绍装饰器前,先来理解一下闭包的概念。

1. 闭包

我们知道,一个函数内部的变量是局部变量,在函数执行结束之后,函数内部的变量就会被销毁,而闭包,则可以使我们能够读取函数内部变量。

比如下面这个示例:

def outer_func():
    msg = "outer info"

    def inner_func():
        print(msg)
        return msg

    return inner_func


func = outer_func()
func()

关于闭包,2023.11.13 百度百科的释义如下:

闭包就是能够读取其他函数内部变量的函数。例如在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。

所以闭包的作用可以是避免全局变量可能带来的维护问题,又能够长久的保存变量。

但是同时,基于这个特性,闭包函数内部的局部变量因为会保持在内存中,不会在调用后被自动清除,所以需要注意其可能带来的内存泄漏的问题。

2. 闭包实现计数器

下面我们使用闭包来实现一个计数器的功能:

def create_counter():
    count = 0

    def add_counter():
        nonlocal count
        count += 1
        return count
    return add_counter

f = create_counter()
print(f())
print(f())
print(f())

这里使用 nolocalcount 变量进行了声明,作用是声明该变量只在函数局部内起作用,也就是 create_counter() 内,所以在 add_counter() 外声明 count 变量之后,在 add_counter() 内可以保存其相应的状态,也就是这里我们的计数功能。

nolocal 关键字是专门定义在闭包内使用的。

相对应的 global 字段时定义的全局变量,这里不多做介绍了。

自由变量

自由变量的含义是指未绑定到本地作用域的变量,比如上面的示例里,countadd_counter() 函数里就是一个自由变量,因为它在外层函数 create_counter() 里定义,但没有在内层的 add_counter() 中定义。

至于为什么在 add_counter() 里对 count 变量进行 nolocal 的声明,是因为修饰的对象类型是 int,与之类似的还有 strtuple,他们都属于不可变类型。

而如果我们闭包的内外部函数里的对象是 list,dict 这种可变类型,那么则不需要使用 nolocal 来进行修饰,比如下面的操作:

def create_counter():
    count_dict = [0]  
    
    def add_counter():        
        count_dict[0] += 1        
        return count_dict[0]    
        
    return add_counter

2、 装饰器

装饰器的作用是在不修改被装饰函数的情况下,给被装饰的函数添加额外的功能。

而装饰器就是基于闭包的操作,不过外层函数传入的参数是被装饰的函数,且在 Python 里,使用装饰器的方式是在被装饰函数前加一行,使用 @ 符号来调用。

最简单的装饰器的操作如下:

def decorator(func):
    print("calling decorator ...")
    return func


@decorator
def test():
    print("calling test ...")

我们在下面的操作中使用一个示例介绍如何基于闭包使用装饰器。

3、装饰器代码示例

前面我们介绍了一个需求场景,需要统计函数的执行时间,基于这个需求,我们就可以使用装饰器的操作来完成,以下是代码示例:

import time

def time_decorator(func):

    def inner_func(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        total_time = time.time() - start_time
        print("func 耗时:", total_time)
        return result
    return inner_func


@time_decorator
def add(x, y):
    time.sleep(1)
    return x + y

add(1, 7)

装饰器原理

我们使用 @ 加上装饰器函数名称,即表示调用这个装饰器,然后将被装饰的函数,上面的示例是 add() 函数,作为参数传入装饰器,然后在内部函数 inner_func() 中添加额外的功能,这里是统计函数运行时间,然后将其返回。

将装饰器的操作扁平化操作,就和前面闭包示例计数器的使用是一致的:

def add(x, y):
    time.sleep(1)
    return x + y

func = time_decorator(add)
func(1, 2)

所以,在加了装饰器的函数运行中,实际上运行的是装饰器的内部函数,我们可以通过打印函数的名称来进行验证:

print(add.__name__)  # inner_func

如果想要保存原函数的基本信息,比如函数名称,我们可以给装饰器的内部函数加上装饰器自动复制函数信息,functools.wraps,使用示例如下:

import time
import functools

def time_decorator(func):

    @functools.wraps(func)
    def inner_func(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        total_time = time.time() - start_time
        print("func 耗时:", total_time)
        return result
    return inner_func

@time_decorator
def add(x, y):
    time.sleep(1)
    return x + y
   
print(add.__name__)  # add

这样打印的就是原始函数的函数名称了。

4、装饰器加参数

如果我们想调用装饰器的时候,给装饰器加一个参数,比如这里的 time_decorator,想加一个默认的时间参数(这个想要实现的功能可能并没有实际意义,纯粹是为了实现给装饰器加默认参数这个功能),调用的时候就是:

@time_decorator(default_time=2)

那么装饰器的定义则如下所示:

def time_decorator(default_time=2):
    def decorator(func):
        def inner_func(*args, **kwargs):
            start_time = time.time()
            time.sleep(default_time)
            result = func(*args, **kwargs)
            total_time = time.time() - start_time
            print("func 耗时:", total_time)
            return result
        return inner_func
    return decorator


@time_decorator(2)
def add(x, y):
    time.sleep(1)
    return x + y

add(1, 8)

如果调用装饰器的时候想使用默认参数,直接不赋值即可:

@time_decorator()
def add(x, y):
    time.sleep(1)
    return x + y

5、多重装饰器

如果我们想要调用多个装饰器来装饰一个函数,其执行顺序是怎么要的呢,我们可以用下面的例子做个实验。

比如我们要做一个汉堡,最外层两片面包,中间夹两片青菜,最中间是一片肉,可以如下操作:

def bread_decorator(func):

    def inner(*args, **kwargs):
        print("先加片面包")
        func(*args, **kwargs)
        print("再加片面包")
    return inner


def vegetable_decorator(func):

    def inner(*args, **kwargs):
        print("先加片蔬菜")
        func(*args, **kwargs)
        print("再加片蔬菜")
    return inner


@bread_decorator
@vegetable_decorator
def make_hamburger():
    print("加片肉")


make_hamburger()

输出的结果为:

先加片面包
先加片蔬菜
加片肉
再加片蔬菜
再加片面包

所以这里装饰器的执行时按照顺序从上到下执行的。

我们可以尝试将装饰器的调用拉平,用到的其实就是设计模式里的装饰器模式了(设计模式的几种类型我回头会更新一个系列),我们先将 make_hamburger() 的函数重新定义,然后调用,bread_decorator()vege_decorator() 还是保持不变:

def make_hamburger():    
    print("加片肉")

food = vegetable_decorator(make_hamburger)
food = bread_decorator(food)
food()

执行的结果和前面使用装饰器的方式调用是一致的。

6、装饰器类

前面介绍的是用函数作为装饰器,我们还可以设计一个类用作装饰器,示例如下:

class TimeLogDecorator:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):

        start_time = time.time()
        result = self.func(*args, **kwargs)
        print(f"函数 {self.func.__name__} 运行时间为:{time.time() - start_time}")
        return result


@TimeLogDecorator
def add(x, y):
    time.sleep(1)
    return x + y


result = add(1, 6)

在类的 __call__ 方法写入我们在函数装饰器的内部函数里的内容即可实现装饰器的功能。

如果想要给类装饰器带参数的话,示例如下:

class TimeLogDecoratorArg:
    def __init__(self, base_gap_time):
        self.base_gap_time = base_gap_time

    def __call__(self, func):

        def inner_func(*args, **kwargs):
            start_time = time.time()
            time.sleep(self.base_gap_time)
            result = func(*args, **kwargs)
            print(f"函数 {func.__name__} 运行时间为:{time.time() - start_time}")
            return result
        return inner_func


@TimeLogDecoratorArg(2)
def add(x, y):
    time.sleep(1)
    return x + y

如果想获取更多相关文章,可扫码关注阅读:


Hunter
27 声望12 粉丝