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

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

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

什么是作用域

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

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

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

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

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

  1. level = 3
  2. def upgrade():
  3. """在当前等级的基础上提升一级"""
  4. level += 1
  5. def cprint():
  6. print('当前等级:' + '*' * level)
  7. upgrade() # UnboundLocalError: local variable 'level' referenced before assignment
  8. cprint() # 当前等级:***
  9. 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”异常:

  1. level = 3
  2. def upgrade():
  3. """在当前等级的基础上提升一级"""
  4. global level # global声明将“level”标记为全局变量
  5. level += 1
  6. upgrade() # 太棒了,没有触发异常!
  7. 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闭包:

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

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

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

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

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

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

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

观察以上例子可以发现:

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

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

  1. import functools
  2. def uppercase(func):
  3. @functools.wraps(func)
  4. def wrapper():
  5. original_result = func() # 引用了uppercase函数作用域中的变量func
  6. modified_result = original_result.upper()
  7. return modified_result
  8. return wrapper
  9. @uppercase
  10. def make_greeting_words():
  11. """来段问候语"""
  12. return 'Hello, World!'
  13. make_greeting_words() # 'HELLO, WORLD!'
  14. make_greeting_words.__name__ # 'make_greeting_words'
  15. make_greeting_words.__doc__ # '来段问候语'

带参数的装饰器:

  1. import functools
  2. def cache(func):
  3. """memorization装饰器,用于提高递归效率"""
  4. known = dict()
  5. @functools.wraps(func)
  6. def wrapper(*args):
  7. if args not in known:
  8. known[args] = func(*args)
  9. return known[args]
  10. return wrapper
  11. @cache
  12. def fibonacci(n):
  13. """计算Fibonacci数列的第n项"""
  14. assert n >= 0, 'n必须大于等于0'
  15. return n if n in {0, 1} else fibonacci(n - 1) + fibonacci(n - 2)
  16. fibonacci(5) # 5
  17. 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. JavaScript day03 循环

    循环 while循环 循环是重复性做一件事情 没有办法控制每次循环的时间长度 循环会增大程序时间复杂度(不建议无限循环嵌套 一般情况下不会嵌套超过两次) 死循环 是不会停止的循环 会导致电脑内存溢出 ...

  2. 对象头源码讲解,原来,指向objectMonitor的指针在这里

    markword 注释 该文件目录在: \openjdk-jdk8u\hotspot\src\share\vm\oops\markOop.hpp #ifndef SHARE_VM_OOPS_MARKO ...

  3. 解释基于 XML Schema 方式的切面实现?

    在这种情况下,切面由常规类以及基于 XML 的配置实现.

  4. Java基础学习之“二维数组”

    一.鄙人对二维数组的理解 二维数组就是由多个数组并列而成 二.举例 1.普通数组(一维数组)的图像格式 2.二维数组的图像格式 代码 1 @Test 2 public void xueXi(){ 3 ...

  5. 2. 使用Github

    2. 使用Github 2.1 目的 借助github托管项目代码 2.2 基本概念 仓库(Repository) 仓库用来存放项目代码,每个项目对应一个仓库,多个开源项目则有多个仓库 收藏(Star ...

  6. 15_伯德图,为什么是20logM?分贝又是什么?_Bode Plot_Part1

  7. 高效使用Java构建工具,Maven篇|云效工程师指北

    大家好,我是胡晓宇,目前在云效主要负责Flow流水线编排.任务调度与执行引擎相关的工作. 作为一个有多年Java开发测试工具链开发经验的CRUD专家,使用过所有主流的Java构建工具,对于如何高效使用 ...

  8. “一键”生成HTML——Emmet插件常用语法

    Emmet是一款文本编辑器/IDE的插件,用来快速生成复杂的HTML代码,只要掌握一些常用的语法(类似于CSS选择器),就可以减少重复编码的工作(主要是懒).我个人惯用的是sublime,因此下文介绍 ...

  9. Flex布局在小程序的使用

    一篇旧文,上手小程序时做的一些探索 Flex布局是一种十分灵活方便的布局方式,目前主流的现代浏览器基本都实现了对Flex布局的完全支持.而在微信小程序中,IOS端使用的渲染引擎WKWebView和安卓 ...

  10. vue全家桶+axios+jsonp+es6 仿肤君试用小程序

    vue全家桶+axios+jsonp+es6 仿肤君试用小程序 把自己写的一个小程序项目用vue来实现的,代码里面有一些注释,主要使用了vue-cli,vue,vuex,vue-router,axoi ...