Python装饰器:套层壳我变得更强了

昨天阅读了《Python Tricks: The Book》的第三章“Effective Functions”,这一章节介绍了Python函数的灵活用法,包括lambda函数、装饰器、不定长参数*args和**kwargs等,书中关于闭包的介绍让我回想起了《你不知道的JavaScript-上卷》中的相关内容。本文主要记录自己在学习Python闭包和装饰器过程中的一些心得体会,部分内容直接摘抄自参考资料。

关于作用域和闭包可以聊点什么?

什么是作用域

作用域负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。换句话说,作用域是根据名称查找变量的一套规则。

作用域的以下两点规则需要特别注意:

  • “遮蔽效应”:作用域查找会在找到第一个匹配的标识符时停止,嵌套作用域内部的标识符会遮蔽外部的标识符;

  • 提升:无论作用域中的声明出现在什么地方,都将在代码本身被执行前首先进行处理,可以形象地认为变量和函数声明从它们在代码中出现的位置被“移动”到了所在作用域的顶部。

下面通过一个例子进行说明:

level = 3

def upgrade():
"""在当前等级的基础上提升一级"""
level += 1 def cprint():
print('当前等级:' + '*' * level) upgrade() # UnboundLocalError: local variable 'level' referenced before assignment
cprint() # 当前等级:***
print(xyz) # NameError: name 'xyz' is not defined

为什么同样是引用全局变量“level”,执行函数“upgrade”触发了“UnboundLocalError”异常,而执行函数“cprint”就不会呢?这是因为在代码编译的过程中,函数“upgrade”的赋值表达式“level += 1”会被解析为“level = level + 1”,这涉及变量声明和变量赋值两个过程。首先是变量声明,“level”会被声明为局部变量(全局作用域里面的“level”被遮盖了),并且它的声明会被提升到函数作用域的顶部;其次是变量赋值,Python解释器会从函数作用域中查询“level”,并计算表达式“level + 1”的结果,由于此时“level”虽然被声明了,但是还没有被赋值(绑定?),计算失败,触发了“UnboundLocalError”异常。

“UnboundLocalError”异常和“NameError”异常的触发条件是不同的:

  • UnboundLocalError: Raised when a reference is made to a local variable in a function or method, but no value has been bound to that variable.

  • NameError: Raised when a local or global name is not found.

从官方文档给出的描述中可以看到,“UnboundLocalError”异常是在变量被声明了(在作用域中找到了)但是还没有绑定值的时候触发,而“NameError”异常是在作用域中找不到变量的时候触发,两者是有比较明显的区别的。

通过为函数“upgrade”中的变量“level”加上global声明可以规避“UnboundLocalError”异常:

level = 3

def upgrade():
"""在当前等级的基础上提升一级"""
global level # global声明将“level”标记为全局变量
level += 1 upgrade() # 太棒了,没有触发异常!
print(level) # 4

global声明将“level”标记为全局变量,在代码编译过程中不会再声明“level”为函数作用域里面的局部变量了。nonlocal声明具有相似的功能,但使用的场景与global不同,由于篇幅限制,这里不再展开说明。

什么是闭包

A closure remembers the values from its enclosing lexical scope even when the program flow is no longer in that scope.

当函数可以记住并访问所在的词法作用域(定义函数时所在的作用域),即使函数是在词法作用域之外执行,这时就产生了闭包。

通过计算移动平均值的例子说明Python闭包:

def make_averager():
"""工厂函数"""
series = []
def averager(new_value):
"""移动平均值计算器"""
series.append(new_value) # series是外部作用域中的变量
total = sum(series)
return total / len(series)
return averager # 返回内部定义的函数averager averager = make_averager()
averager(10) # 10
averager(20) # 15
averager(30) # 20

可以看到函数“averager”的定义体中引用了工厂函数“make_averager”的词法作用域中的局部变量“series”,当“averager”被当作对象返回并且在全局作用域中被调用,它仍然能够访问“series”的值,据此计算移动平均值。这就是闭包。

Python在函数的“__code__”属性中保存了词法作用域中的局部变量和自由变量(free variable,“series”就是自由变量)的名称,在函数的“__closure__”属性中保存了自由变量的值:

averager.__code__.co_varnames  # ('new_value', 'total')
averager.__code__.co_freevars # ('series',)
averager.__closure__ # (<cell at 0x000002135DE72FD8: list object at 0x000002135D589488>,)
averager.__closure__[0].cell_contents # [10, 20, 30]

装饰器:套层壳我变得更强了

装饰器常用于把被装饰的函数(或可调用的对象)替换成其他函数,它的输入参数是一个函数,输出结果也是一个函数。装饰器是实现横切关注点(cross-cutting concerns)的绝佳方案,使用场景包括数据校验(用户登录了吗?用户有权限访问数据吗?)、缓存(functools.lru_cache)、日志打印等。

def uppercase(func):
def wrapper():
original_result = func() # 引用了uppercase函数作用域中的变量func
modified_result = original_result.upper()
return modified_result
return wrapper def make_greeting_words():
"""来段问候语"""
return 'Hello, World!' greet = uppercase(make_greeting_words) # 用uppercase装饰make_greeting_words
greet() # 'HELLO, WORLD!',好耶,单词变成大写的了!
greet.__name__ # 'wrapper'
greet.__doc__ # None

观察以上例子可以发现:

  1. 装饰器的输入是一个函数,输出也是一个函数;
  2. 被装饰的函数的一些元信息(原始函数名、文档字符串)被覆盖了;
  3. 装饰器基于闭包。

Python提供了通过@decorator_name的方式使用装饰器的语法糖。此外,通过使用functools.wraps(func),被装饰的函数的元信息能够得以保留,这有助于代码的调试:

import functools

def uppercase(func):
@functools.wraps(func)
def wrapper():
original_result = func() # 引用了uppercase函数作用域中的变量func
modified_result = original_result.upper()
return modified_result
return wrapper @uppercase
def make_greeting_words():
"""来段问候语"""
return 'Hello, World!' make_greeting_words() # 'HELLO, WORLD!'
make_greeting_words.__name__ # 'make_greeting_words'
make_greeting_words.__doc__ # '来段问候语'

带参数的装饰器:

import functools

def cache(func):
"""memorization装饰器,用于提高递归效率"""
known = dict() @functools.wraps(func)
def wrapper(*args):
if args not in known:
known[args] = func(*args)
return known[args]
return wrapper @cache
def fibonacci(n):
"""计算Fibonacci数列的第n项"""
assert n >= 0, 'n必须大于等于0'
return n if n in {0, 1} else fibonacci(n - 1) + fibonacci(n - 2) fibonacci(5) # 5
fibonacci(50) # 12586269025

参考资料

  1. Python Tricks: The Book
  2. 《你不知道的JavaScript-上卷》第一部分“作用域和闭包”
  3. 《流畅的Python》第7章“函数装饰器和闭包”
  4. Python UnboundLocalError和NameError错误根源解析
  5. Built-in Exceptions
  6. 《精通Python设计模式》第5章“修饰器模式”

Python装饰器:套层壳我变得更强了的更多相关文章

  1. Python装饰器由浅入深

    装饰器的功能在很多语言中都有,名字也不尽相同,其实它体现的是一种设计模式,强调的是开放封闭原则,更多的用于后期功能升级而不是编写新的代码.装饰器不光能装饰函数,也能装饰其他的对象,比如类,但通常,我们 ...

  2. 理解 Python 装饰器看这一篇就够了

    讲 Python 装饰器前,我想先举个例子,虽有点污,但跟装饰器这个话题很贴切. 每个人都有的内裤主要功能是用来遮羞,但是到了冬天它没法为我们防风御寒,咋办?我们想到的一个办法就是把内裤改造一下,让它 ...

  3. Python 装饰器初探

    Python 装饰器初探 在谈及Python的时候,装饰器一直就是道绕不过去的坎.面试的时候,也经常会被问及装饰器的相关知识.总感觉自己的理解很浅显,不够深刻.是时候做出改变,对Python的装饰器做 ...

  4. Python——装饰器(Decorator)

    1.什么是装饰器? 装饰器放在一个函数开始定义的地方,它就像一顶帽子一样戴在这个函数的头上.和这个函数绑定在一起.在我们调用这个函数的时候,第一件事并不是执行这个函数,而是将这个函数做为参数传入它头顶 ...

  5. Python装饰器,Python闭包

    可参考:https://www.cnblogs.com/lianyingteng/p/7743876.html suqare(5)等价于power(2)(5):cube(5)等价于power(3)(5 ...

  6. Python装饰器与面向切面编程

    今天来讨论一下装饰器.装饰器是一个很著名的设计模式,经常被用于有切面需求的场景,较为经典的有插入日志.性能测试.事务处理等.装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量函数中与函数 ...

  7. python装饰器总结

    一.装饰器是什么 python的装饰器本质上是一个Python函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象.简单的说装饰器就是一个用来返回函数的函数 ...

  8. python 装饰器 一篇就能讲清楚

    装饰器一直是我们学习python难以理解并且纠结的问题,想要弄明白装饰器,必须理解一下函数式编程概念,并且对python中函数调用语法中的特性有所了解,使用装饰器非常简单,但是写装饰器却很复杂.为了讲 ...

  9. Python第二十六天 python装饰器

    Python第二十六天 python装饰器 装饰器Python 2.4 开始提供了装饰器( decorator ),装饰器作为修改函数的一种便捷方式,为工程师编写程序提供了便利性和灵活性装饰器本质上就 ...

随机推荐

  1. 『现学现忘』Docker基础 — 31、实现MySQL同步数据

    目录 1.搜索镜像 2.拉取镜像 3.查看镜像 4.启动镜像 5.操作容器 (1)在MySQL中创建数据库 (2)外部连接Dokcer容器中的MySQL服务 (3)查看挂载情况 (4)测试MySQL服 ...

  2. jQuery--基本事件总结

    基本事件介绍 blur() 失去焦点 change() 改变(select) click() 单机 dbclick() 双击 error() 页面异常 focus() 获得焦点 focusin() j ...

  3. 说出 5 个 JDK 1.8 引入的新特性?

    Java 8 在 Java 历史上是一个开创新的版本,下面 JDK 8 中 5 个主要的特性: Lambda 表达式,允许像对象一样传递匿名函数 Stream API,充分利用现代多核 CPU,可以写 ...

  4. 一个注解@Recover搞定丑陋的循环重试代码

    使用背景 在实际项目中其中一部分逻辑可能会因为调用了外部服务或者等待锁等情况下出现不可预料的异常,在这个时候我们可能需要对调用这部分逻辑进行重试,代码里面主要就是使用for循环写一大坨重试的逻辑,各种 ...

  5. DateFormat类,利用SimpleDateFormat解决系统时间初始(格式化/解析)问题

    目标: java.text.DateFormat 是日期/时间格式化子类的抽象类,我们通过这个类可以帮我们完成日期和文本之间的转换,也就是可以在Date对象与String对象之间进行来回转换. 格式化 ...

  6. python模块 | 时间处理模块—datetime模块

    在python中,与时间处理相关的模块有 time,datetime,calendar. 时间的两个概念: UTC(世界协调时): 整个地球分为二十四时区,每个时区都有自己的本地时间.格林威治天文时间 ...

  7. 汽车最强大脑ECU和单片机是什么关系

    先上图一张,据说这是某个F1赛车的动力总成ECU. 定睛一看,这不就是两个英飞凌的单片机的合体嘛. ECU的定义 ECU原来指的是engine control unit,即发动机控制单元,特指电喷发动 ...

  8. 顺利通过EMC实验(11)

  9. visual studio 2019工具里添加开发中命令提示符的方法

    最新新装了visual studio 2019,发现默认的没有开发者命令提示符 现将添加步骤描述如下: 从VS2019菜单选择"Tools",然后选择"外部工具" ...

  10. 设备像素,CSS像素,设备独立像素

    1.概念 设备像素(device pixel)简写DP 设备像素又称 **物理像素** ,是设备能控制显示的最小单位,我们可以把它看做显示器上的一个点.我们常说的 1920x1080像素分辨率就是用的 ...