4

Python装饰器:套层壳我变得更强了! - Wan-deuk-i

 2 years ago
source link: https://www.cnblogs.com/wan-deuk/p/16169063.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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章“修饰器模式”

Recommend

  • 67

    1、装饰器1.1、装饰器定义装饰器:定义:本质上是函数,(装饰器的功能:装饰其他函数),就是为其他函数添加附加的功能。原则:1.不能修改被装饰的函数的源代码2.不能修改被装饰的函数的调用方式简单装饰器程序演示:计算test1函数的运行时间importtimedeftimemer...

  • 31
    • zhuanlan.zhihu.com 6 years ago
    • Cache

    Python 3.7 将引入 dataclass 装饰器

    简评:Python 3.7 将于今年夏天发布,Python 3.7 中将会有许多新东西,最激动人心的新功能之一是 dataclass 装饰器。什么是 Data Class大多数 Python 开发人员编写过很多像下面这样的类:class MyClass: def __in…

  • 23
    • 微信 mp.weixin.qq.com 5 years ago
    • Cache

    Python之装饰器

  • 36
    • 微信 mp.weixin.qq.com 5 years ago
    • Cache

    Python 之装饰器

    1.认识装饰器 在python中,对于一个函数,若想在其运行前后做点什么,那么装饰器是再好不过的选择,话不多说,上代码。 这段代码,初看之下,确实不是很理解,接下来一步一步分析,看看装饰器到底是怎么工作的。...

  • 42

  • 57
    • www.zlovezl.cn 5 years ago
    • Cache

    Python 工匠:使用装饰器的技巧

    前言 这是 “Python 工匠”系列的第 8 篇文章。 [查看系列所有文章]

  • 32

  • 3

    痛心:实验室服务器被挖矿怎么办? 实验室的服务器有比较高的配置,安装了多块显卡,平时实验室的同学主要通过PuTTY、Xshell以及MobaXterm等工具远程访问服务器,上传代码跑实验。过去有段时间,服务器的显卡总是被挖矿程序占用,学校信息中心的老师...

  • 0
    • www.cnblogs.com 2 years ago
    • Cache

    你不知道的Python容器 - Wan-deuk-i

    你不知道的Python容器 昨天阅读了《Python Tricks: The Book》的第五章“Common Data Structures in Python”,该章节介绍了字典、数组、集合、栈、队列以及堆等数据结构的用法和注意事项,其中ChainMap、MappingProxyType等不常使用的容器类引起了我的...

  • 8

    各个gui开发工具对比 Flexx: 可以使用Flexx创建桌面应用程序和web应用程序,同时可以将程序导出到独立的HTML文档中,GitHub推荐 Kivy&BeeWare: 只需编写一套代码便可轻松运行于各大移动平台和桌面上,像Android,iOS,L...

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK