UP | HOME

Python 闭包和装饰器

目录

1 简介

Python 中, 适当的使用 闭包装饰器 可以有效的提高编码效率, 同时编写出更加简介的代码。

所以用这篇博客来总结对 闭包装饰器 的理解。

2 闭包

闭包 的概念可以通过查阅 维基百科 进行了解, 其中的一种说法感觉和 Python 中的 闭包 的实现更贴近:

  • 闭包是由函数和与其相关的引用环境组合而成的实体.

这种说法的理解可以通过代码来尝试理解:

In [1]: def func(num):
   ...:     def inner_func():
   ...:         return num
   ...:     return inner_func
   ...:

In [2]: f = func(10)

In [3]: f
Out[3]: <function __main__.func.<locals>.inner_func>

这里实现了一个简单的 闭包, 调用函数 func 的时候会返回在 func 中定义的嵌套函数 inner_func.

在函数 inner_func 中使用变量 num, 这个变量来自函数 func.

可以看到第三行的输出

<function __main__.func.<locals>.inner_func>

可以看到字段 func.<locals>.inner_func, 这说明 inner_func 是位于 func 的局部命名空间中的。

返回的 inner_func 除了函数体本身外, 还与 func 的局部命名空间存在联系。

现在换一个方式来进行测试:

In [7]: def func(lst=[]):
   ...:     def inner_func():
   ...:         lst.append(10)
   ...:         return lst
   ...:     return inner_func
   ...:

In [8]: f1 = func()

In [9]: f2 = func()

In [10]: f1()
Out[10]: [10]

In [11]: f2()
Out[11]: [10, 10]

In [12]: id(f1) == id(f2)
Out[12]: False

Python 中, 函数 也是一个 引用类型 的对象, 而 引用类型 的对象只会存在唯一的一个 实体.

因此函数 func局部命名空间 也是唯一的。

Out[12] 可以看到, f1f2 并不是同一个对象, 而分别调用 f1f2 的输出却产生了影响。

说明 f1f2 共用函数 func 的局部命名空间, 即: 函数 f1f2 是和函数 func 的局部命名空间组合而成的实体。

总结: 闭包 函数是 函数体 和定义该 函数体局部命名空间 共同组成的。 闭包 函数可以调用该 局部命名空间 的内容。

PS: 其实感觉如果像这样理解的话, 从模块中导入一个对象的方式也很像 闭包.

3 装饰器

可以说, 装饰器 就是 闭包 的一种应用, 因为装饰器往往是将 函数 作为参数传入, 然后返回一个 函数, 这个函数和 装饰器 的环境存在联系。

当然, 装饰器被装饰 的对象不一定必须是一个 函数. 只需要满足 装饰器被装饰对象 都是一个 可调用 的对象即可。

这也是 Python 中存在各种各样的 装饰器 的一个原因。

为了方便理解, 可以根据 装饰器 的使用方式将 装饰器 分为两类:

  • 使用时 不带参 的装饰器
  • 使用时 带参 的装饰器

注意:使用时 不带参.

@decorator  # 不带参
def func():
    pass

@decorator(level=3)  # 带参
def func():
    pass

这两种类型的 装饰器 的调用流程存在一些区别:

不带参的装饰器的调用流程
  1. 被装饰对象 作为 参数 传入 装饰器
  2. 装饰器执行 完内部流程后返回一个 可调用对象
  3. 这个 可调用对象 就是我们使用的 被装饰函数 或其他对象

等价于: func = decorator(func).

带参数的装饰器的调用流程
  1. 将设置的 参数 传入 装饰器
  2. 装饰器执行 完内部流程后返回一个 可调用对象A
  3. 被装饰对象 作为参数传入返回的这个 可调用对象A
  4. 可调用对象A执行 完内部流程后返回一个 可调用对象 B
  5. 这个 可调用对象B 就是我们使用的 被装饰函数 或其他对象

可以看到, 两种 装饰器 的调用流程的区别很明显, 带参数不带参数 多了一次调用流程。

并且每次调用流程都是 执行 了函数体内部的 内容 的。

这两种 装饰器 的区别主要是在 调用流程 上, 对于 装饰器 内部的代码并没做过多的要求。

因此, 你可以写一个 装饰器, 这个 装饰器 接受 带参不带参 两种调用方式:

def decorator(func=None, **kwargs):
    def wrap_arg(func):
        def wrap():
            func()
        return wrap

    def wrap():
        func()

    if func is None:
        return wrap_arg

    return wrap

实现这样的 装饰器 的关键是 关键字 参数和 第一个 参数的设置。

如果 不带参 调用 装饰器, 那么被装饰的对象会作为 装饰器 的第一个参数传入。

如果 带参 调用 装饰器, 那么首先传入的 参数 会是你设置的参数, 而由于 装饰器 的参数设置为了 关键字 参数, 因此 第一个 参数 func 会被置为 None.

此时根据相关的 调用流程 选择需要返回的 对象 即可。

当然, 上面的这个实现方式很不规范, 常用的实现方式是结合标准库 functools 来定义 装饰器.

比如使用 functools.wraps 来保留函数元信息, 使用 functools.partial 来创建可选参数的 装饰器.

这两个函数的使用可以浏览:

3.1 多重装饰器

使用多个 装饰器 对一个对象进行装饰的时候, 靠近 对象 的装饰器优先调用。

@dec_a
@dec_b
@dec_c
def func():
    pass

像上面的代码, 会首先调用装饰器 dec_c, 然后依次调用 dec_bdec_a.

但是, 需要注意一下被装饰的函数在调用时执行的顺序:

  • 装饰器的调用顺序为 dec_c -> dec_b -> dec_a
  • 那么装饰的层次就为 dec_a -> dec_b -> dec_c, 即: dec_a(dec_b(dec_c(func))).
  • 定义装饰器常用的方式是返回一个函数, 返回的函数体中除了执行 func 以外, 往往还有其他操作
  • 因此, 调用 func 的时候, 会首先执行 dec_a 返回的函数体中的操作, 然后是 dec_b, dec_cfunc.

所以, 在使用多重装饰器的时候需要分清楚 装饰 的顺序和 调用 的顺序。

4 参考链接

版权声明:本作品采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可