深入理解Python装饰器与闭包

2023-11-01

最初学习Python时,了解到装饰器与闭包的概念,在网上看了很多博客与教程,总觉得自己的理解还是不那么透彻,最近开始学习《流畅的Python》一书,书中对与闭包和装饰器有详细的解释,我觉得写的非常到位,现在把我的理解分享出来与大家共同探讨。

装饰器

函数装饰器用于在源码中“标记”函数,以某种方式增强函数的行为。装饰器是一个可调用的对象,其参数是另一个函数(即被装饰的函数)。装饰器可能会处理被装饰的函数,然后把它返回,或者将其替换为另一个函数或可调用对象。

假如有一个名为decorate的装饰器如下:

@decorate
def target():
    print("running target()")

上述代码的效果与下述一样:

def target():
    print("running target()")

target = decorate(target)

两种写法的最终结果一样:上述两个代码片段执行完毕之后得到的target不一定是原来的那个target函数,而是decorate(target)返回的函数。
为了确认被装饰的函数会被替换,请看下述代码输出结果:

def deco(func):
    def inner():
        print("running inner()")
    return inner

@deco
def target():
    print("running target()")

target() # 运行target函数
print(target) # 输入target对象,可以看到target现在其实已经是inner的引用了

输出结果如下:

running inner()
<function deco.<locals>.inner at 0x0000024EF95041F8>

上述代码中,我们执行了如下操作:

  1. deco返回inner函数对象
  2. 使用deco装饰target
  3. 调用被装饰的target其实会运行inner
  4. 审查对象,发现target现在是inner的引用

事实上,多数装饰器会修改被装饰的函数。通常,它们会定义一个内部函数,然后返回它以替换被装饰的函数。使用内部函数的代码几乎都要靠闭包才能正确的运作。为了理解闭包,我们先退一步,先了解Python中变量的作用域。

变量作用规则

在下述代码中,我们定义了一个函数,它读取两个变量的值:一个局部变量a,是函数的参数;另一个是变量b,在这个函数中我们没有定义它。

def f1(a):
    print(a)
    print(b)

f1(3)

运行结果如下:

Traceback (most recent call last):
  File "E:/Coding/PyProject/Practices/Day1/Part4.py", line 5, in <module>
    f1(3)
  File "E:/Coding/PyProject/Practices/Day1/Part4.py", line 3, in f1
    print(b)
NameError: name 'b' is not defined
3

事实上,出现这个错误我们不会感到奇怪,稍稍了解编程规则的同学都能很清楚的知道这个错误为何。我们只需要在调用f1之前给全局变量b赋值,这个函数就不会出现任何错误,像下面这样:

def f1(a):
    print(a)
    print(b)

b = 1
f1(3)
3
1

很好的输出了a,b的值,符合我们的预期,我们再来看下下面的代码,可能结果会让我们吃惊:

b = 1
def f1(a):
    print(a)
    print(b)
    b = 9

f1(3)

运行结果如下:

Traceback (most recent call last):
  File "E:/Coding/PyProject/Practices/Day1/Part4.py", line 8, in <module>
    f1(3)
  File "E:/Coding/PyProject/Practices/Day1/Part4.py", line 4, in f1
    print(b)
UnboundLocalError: local variable 'b' referenced before assignment
3

我们在print(b)之前对b进行了定义,但是当执行完第一个print(a)之后,明明应该打印b的值1,但是控制台却出现了报错。

事实上,Python编译函数的定义体时,它判断b是局部变量,因为在函数中给它赋值了,于是Python会尝试从局部空间中获取b。当我们调用f1(3)的时候,f1的定义体会获取并打印局部变量a的值,但是尝试获取局部变量b的值时,发现b没有绑定值,于是就出现了报错。

Python不要求声明变量,但是假定在函数定义体中赋值的变量时局部变量。因此,如果在函数中赋值时想让解释器把b当作全局变量,要使用global声明:

b = 1
def f1(a):
    global b
    print(a)
    print(b)
    b = 9

f1(3)

这样,我们再去运行这段代码时,就不会出现任何的错误信息。

闭包

在《流畅的Python》一书中,作者对闭包的定义是这样的,“闭包指延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的非全局变量”,意思就是值闭包函数可以访问函数定义体之外的非全局变量。看起来很难理解,请看下面的例子:

我们定义一个avg函数,用来返回不断增加的一系列数的平均值。
我们可以很简单的这样来实现这个功能:

class averager():

    def __init__(self):
        self._data_list = []

    '''
        在自定义类中实现__call__()函数可以将实例变成可调用的对象,
        在接下来的调用中我们可以很直观的看到该函数作用
    '''
    def __call__(self, new_value):
        self._data_list.append(new_value)
        total = sum(self._data_list)
        return total / len(self._data_list)

# 得益于__call__()函数,我们可以直接调用该类的实例
avg = averager()
print(avg(5))
print(avg(6))
print(avg(7))

输出如下:

5.0
5.5
6.0

那么,用函数式编程如何实现呢,我们看下面的示例:

def averagerPlus():
    data_list = []

    def averager(new_value):
        data_list.append(new_value)
        total = sum(data_list)
        return total / len(data_list)

    return averager

avg = averagerPlus()
print(avg(5))
print(avg(6))
print(avg(7))

输出结果如下:

5.0
5.5
6.0

上面两个例子有共同之处:调用averager()或者averagerPlus()时,都会得到一个可调用的对象avg,它会更新历史值,然后计算当前的平均值。

averager类的实例avg如何存储历史值我们可以很明显的看到:self._data_list实例属性。但是averagerPlus中的avg函数如何去寻找data_list呢?

注意,data_list是函数averagerPlus的局部变量,因为在averagerPlus的定义体中初始化了data_list == []。但是,当我们调用avg(5)时,averagerPlus函数已经返回了,而它的本地作用域也一去不复返了。

在averagerPlus函数中,data_list是自由变量,即指的是在本地作用域中未绑定的变量。如下图所示:
在这里插入图片描述
因此,在内部函数averager中,我们实际上绑定了averagerPlus中初始化的自由变量data_list。

综上,闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时,虽然定义域不可用了,但是仍然能够使用这些绑定,这也是闭包的灵魂所在。

nonlocal声明

实际上,我们在上面实现的averagerPlus函数的效率并不高,因为每次我们都会去重新计算一遍整个数组的sum值。更好的做法是,我们只用两个值来保存当前的元素综合和当前元素的总个数。

重新实现averagerPlus函数如下:

def averagerPlus():
    count = 0
    total = 0

    def averager(new_value):
        count += 1
        total += new_value
        return total / count

    return averager

avg = averagerPlus()
print(avg(5))
print(avg(6))
print(avg(7))

但是,这个函数显然没有像我们想象中那样得到运行结果,而是出现了如下运行结果:

Traceback (most recent call last):
  File "E:/Coding/PyProject/Practices/Day1/Part5.py", line 13, in <module>
    print(avg(5))
  File "E:/Coding/PyProject/Practices/Day1/Part5.py", line 6, in averager
    count += 1
UnboundLocalError: local variable 'count' referenced before assignment

这又是为什么呢?

这就牵扯到Python中可变与不可变对象的问题,我们知道,在Python中,int型其实是属于不可变的类型,在我们执行 count += 1实际就相当于执行了 count = count + 1 。因此,我们在averager的定义体中为count赋值了,这会把count变成局部变量,total也是如此。

那么在之前的示例中为什么没有这个问题呢?因此在之前,我们并没有给data_list赋值,我们只是调用了append()方法不断的往数组中追加元素,并把它传给sum和len,也就是说,我们利用了列表是可变的对象这一事实。

但是对数字、字符串、元组等不可变类型来说,只能读取,不能更新。如果尝试重新绑定,例如上面的count += 1这样的操作,其实会导致Python解释器隐式的创建局部变量count。这样,count就不是自由变量了,因此不能保存在闭包中。

为了解决这个问题,Python引入了nonlocal声明。它的作用是把变量标记为自由变量,即使在函数中为变量赋予新值了,也会变为自由变量。如果nonlocal声明的变量赋予新值,闭包中保存的绑定会更新。我们利用nonlocal重新实现averagerPlus函数如下:

def averagerPlus():
    count = 0
    total = 0

    def averager(new_value):
        nonlocal count, total
        count += 1
        total += new_value
        return total / count

    return averager

avg = averagerPlus()
print(avg(5))
print(avg(6))
print(avg(7))

完美运行。

总结

闭包和装饰器是很好用的特性,能让我们轻松的实现对函数功能的拓展而无需去改动原函数。初学者可能刚理解起来会困难一点,但是,看完本文之后,一定会豁然开朗!

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

深入理解Python装饰器与闭包 的相关文章

随机推荐

  • Bugku 计算器

    首先打开题目链接 发现一个式子 但答案有三位数 而只能输入一个数字 直接F12查看原代码 发现maxlenthen 1 maxlenthen意思是文件域可接受的字符数量的上限 可输入字符串最大的长度 容质 所以把1改为3就好啦 然后得到fl
  • Redisson源码-多线程之首个获取锁的线程加解锁流程

    Redisson源码 多线程之首个获取锁的线程加解锁流程 简介 当有多个线程同时去获取同一把锁时 第一个获取到锁的线程会进行加解锁 其他线程需订阅消息并等待锁释放 以下源码分析基于redisson 3 17 6版本 不同版本源码会有些许不同
  • openEuler 20.03 LTS SP2以及SP3安装完gnome后,gdm登陆进入不了桌面问题

    一 问题原因 是由于CVE 2020 17489相关补丁引入的 暂不清楚是何原因造成 但除去该相关补丁之后 该问题消失 在网上查了下 CVE 2020 17489的问题是gnome shell的某些配置中会发现 注销账户时 登陆对话框中的密
  • SQLI-Labs(15-17)

    目录 15关 16关 17关 15关 看到这个那么我们可以首先尝试报错或者盲注 payload or length database 8 qwe 在这里我们发现and 会报错 跟之前我们利用and爆错不一样 那为什么这里我们在post传参时
  • Dubbo是什么

    Dubbo是什么 Dubbo是一个分布式服务框架 致力于提供高性能和透明化的RPC远程服务调用方案 以及SOA服务治理方案 简单的说 dubbo就是个服务框架 如果没有分布式的需求 其实是不需要用的 只有在分布式的时候 才有dubbo这样的
  • 统计oracle 数据库 lawpeople表lawtype字段多个值只统计一次问题,按照地区分类

    select temparea name case when lawtype like 501 then 501 when lawtype like 502 then 502 when lawtype like 503 then 503 w
  • CSR867x — 如何看懂一份psr文件

    XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX XX 作 者 文化人 XX 联系方式 XX 版权声明 原创文章 欢迎评论和转载 转载时能告诉我一声就最好了 XX 要说的话 作者
  • Dubbo路由规则:静态标签的使用与扩展

    一 路由的流程 路由是通过互联网把信息从源地址传输到目的地址的过程 而决定路由目标地址的是路由规则 在Dubbo里 路由规则在发起一次RPC调用前起到过滤目标服务器地址的作用 过滤后的地址列表 将作为消费端最终发起RPC调用的备选地址 它能
  • LeetCode 62. 不同路径

    欢迎来到茶色岛独家岛屿 本期将为大家揭晓LeetCode 62 不同路径 做好准备了么 那么开始吧 一 题目名称 LeetCode 62 不同路径 二 题目要求 一个机器人位于一个 m x n 网格的左上角 起始点在下图中标记为 Start
  • git 导出版本之间差异文件

    查看 commit id 首先用 git log 查看版本库日志 找出需要导出的 commit id git log pretty oneline 456bcbccd91278f7fdf6bf11bc73c4e3a6193c7f HEAD
  • 基于深度神经网络的社交媒体用户级心理压力检测

    User Level Psychological Stress Detection from Social Media Using Deep Neural Network 基于深度神经网络的社交媒体用户级心理压力检测 ABSTRACT It
  • 软件anyconnec-win安装下载

    anyconnec win介绍 1 安装下载地址 http www drv5 cn sfinfo 14287 html softdown 找到适合自己操作系统的版本 下载并安装 2 直接安装下载点击next就ok了 需要注意的是 下载安装完
  • IDEA小技巧

    IDEA小技巧 常用快捷键 Alt Insert 可以自动生成get set toString方法 Alt Enter 可以帮助解决各种报错 抛个异常啊 导个包啊之类的 常见行操作 Shift Enter 添加空行 相比普通换行 不管光标在
  • [Pytorch系列-48]:如何查看和修改预定义神经网络的网络架构、网络参数属性

    作者主页 文火冰糖的硅基工坊 文火冰糖 王文兵 的博客 文火冰糖的硅基工坊 CSDN博客 本文网址 https blog csdn net HiWangWenBing article details 121342500 目录 第1章 Fin
  • python简单作图的一些设置(4.11课堂笔记)

    1图片布局 1 画布大小 宽 高 英寸 A4 21cm 29 7cm 约7英寸 还要减去页边距 fig plt figure figsize 4 7 2 画图 纸的形状决定图的形状 2 1 不能控制图的形状 ax fig add subpl
  • 力扣每日一题:915. 分割数组【思维题】

    给定一个数组 nums 将其划分为两个连续子数组 left 和 right 使得 left 中的每个元素都小于或等于 right 中的每个元素 left 和 right 都是非空的 left 的长度要尽可能小 在完成这样的分组后返回 lef
  • Redis崩了,我成功把锅甩给了隔壁组

    项目起不来了 项目又起不来了 又双叒叕 上周经常听到组里同事说项目又双叒叕挂了 Redis连不上 笔者在另一套正常的环境忙着开发新需求 没空关心这个问题 PS 反正我的环境能用 先忙完我的再说 于是乎 看了一眼日志 连接数过多 emmm 顺
  • nodejs格式化输入

    需求 比如我现在要格式为Axxx xxx xxx是数字 的格式 但是输入有可能为A1 2这种情况 就需要补零 变成A001 002 代码实现 const regex A d d 正则匹配桩号合法格式 const match input ma
  • baidu 百度搜索 命令

    命令 含义 双引号 xx 关键字全匹配 减号 xxx 排除xxx关键字 Inurl xxx 在url中匹配关键字 intitle xxx title标签中进行匹配 Site xxx 指定域名下搜索 Filetype txt 指定文件类型 例
  • 深入理解Python装饰器与闭包

    最初学习Python时 了解到装饰器与闭包的概念 在网上看了很多博客与教程 总觉得自己的理解还是不那么透彻 最近开始学习 流畅的Python 一书 书中对与闭包和装饰器有详细的解释 我觉得写的非常到位 现在把我的理解分享出来与大家共同探讨