第7章 函数装饰器和闭包

装饰器用于在源码中“标记”函数,动态地增强函数的行为。

了解装饰器前提是理解闭包。

闭包除了在装饰器中有用以外,还是回调式编程和函数式编程风格的基础。

1. 装饰器基础知识

  1. 装饰器是callable对象,其参数是被装饰的函数。
  2. 装饰器将被装饰的函数处理后返回,或者将被装饰的函数替换成另一个函数或可调用对象。
  3. Python也支持类装饰器,参见第21章。

    4. 第一大特性:装饰器能把被装饰的函数替换成其他函数

    5. 第二大特性:装饰器在加载模块时立即执行。看例子3,和下一小节详细说明

    6. 严格来说,装饰器只是语法糖。

例子1. 效果一样的写法:

#假设有个名为decorate的装饰器
@decorate
def target():
pass
def target():
pass target = decorate(target)

例子2. 装饰器通常把函数替换成另一个函数

#deco函数返回inner函数对象
def deco(func):
def inner():
print('running inner()')
return inner #使用deco装饰器装饰target
@deco
def target():
print('running target()') #调用被装饰的target()会运行inner()
target()
#查看target地址,其实是inner()的引用
target

例子3. 装饰器在加载模块时立即执行

可以看出I am deco和running traget()立即输出。

2. Python何时执行装饰器

  1. 装饰器的第二大特性:它们在被装饰的函数定义之后立即运行(联想到Flask框架中的URL映射和绑定)。通常是在导入时(即Python加载模块时)。
  2. 大多数装饰器会在内部定义一个函数,然后将其返回。但有些装饰器返回被装饰的函数,很多Python Web框架使用这样的装时期把函数添加到注册处,例如把URL模式映射到生成HTTP响应的函数上的注册处。这种注册装饰器可能会也可能不会修改被装饰的函数。

3. 使用装饰器改进“策略”模式

  1. 改进第6章的5.1。
  2. 原本的hardcode,当有新的策略(新的promotion)的时候,要手动添加。现在用装饰器装饰新的策略,自动添加进列表。
  3. 优点1:促销策略函数无需使用特殊的名称(例如不同_promo结尾)
  4. 优点2: 临时禁用某个促销策略,只需要把装饰器注释掉。
  5. 优点3:促销折扣策略可以在其他模块中定义,模块化。
promos = []
def promotion(promo_func):
promos.append(promo_func)
return promo_func @promotion
def pro1():
pass @promotion
def pro2():
pass #类也可以被装饰
@promotion
class pro3():
pass #注意,方法和类存入列表的形式不同
#选择最佳策略的函数不变
def best_promo():
"""选择可用的最佳折扣"""
return max(promo(order) for promo in promos)

4. 变量作用域

  1. 为理解闭包,先了解Python中的变量作用域。

例子1. b是局部变量,因为在函数的定义体中给它赋值了

b = 6
#到print(b)时会报错
def f2(a):
print(a)
print(b)
b = 9

原因:

在函数中给b赋值了,Python认为它是局部变量。当获取并打印局部变量a后,尝试获取局部变量b的值时,发现b没有绑定值。

1. 这不是缺陷,这是设计选择:Python不要求声明变量,但是假定在函数定义体中赋值的变量是局部变量。

2. 这比JavaScript的行为好多了,JavaScript也不要求声明变量(在函数中使用var关键字进行显式申明的变量是作为局部变量,而没有用var关键字,使用直接赋值方式声明的是全局变量),但是如果忘记把变量声明为局部变量(使用var),可能在不知情的情况下获取全局变量。

例子2. 在函数中,如果让解释器把b当成全局变量,要使用global声明:

b = 6
def f(a):
global b
print(a)
print(b)
b = 9 f(3) # 3 9 b # 9

例子3. 用dis.dis查看例子1和2的字节码的不同

#反汇编模块
from dis import dis
dis(s) #LOAD_FAST是读本地变量进栈,LOAD_GLOBAL读取全局变量, 区别在b处的操作不同。

5. 闭包(closure)

  1. 闭包是指延伸了作用域的函数,其中包括函数定义体中的引用、定义体之外的非全局变量。
  2. 闭包和匿名函数:在内部里定义函数,只有涉及嵌套函数才有闭包问题。内部函数是不是匿名没有关系。
  3. 闭包关键是能访问定义体之外定义的非全局变量(自由变量)。

问题:自定义avg函数,计算不断增加的系列值的均值,关键是如何保存历史值

方案1: 用类实现计算平均值

class Average():
def __init__(self):
#实例初始化时初始化一个列表
self.series = [] def __call__(self, new_value):
self.series.append(new_value)
#归约函数sum()
total = sum(self.series)
return total/len(self.series) avg = Average()
print(avg(10))
print(avg(11))
print(avg(12))

例子2. 函数式实现,计算平均值的高阶函数

def make_average():
#series是make_average的局部变量,因为在这个函数定义体中初始化了series
series = [] def average(new_value):
series.append(new_value)
total = sum(series)
return total/len(series) return average

例子1在self.series实例属性存储历史值。例子2在自由变量(free variable)series中存历史值。

总结:

  1. 闭包是引用了自由变量的函数。
  2. In computer programming, the term free variable refers to variables used in a function that are not local variables nor parameters of that function
  3. free variable: variables that are used locally, but defined in an enclosing scope

6. nonlocal声明

例子1. 有缺陷的计算平均值的高阶函数

def make_average():
count = 0
total = 0 def average(new_value):
count += 1
total += new_value
return total/count return average avg = make_average()
#报错,当count是不可变类型时,因为count += 1 相当于 count = count + 1。我们在average的定义体中为count赋值,Python当成它是局部变量,total也受到这个问题影响。
#series.append没遇到这个问题,因为利用了列表是可变对象这一事实,没有给series赋值。
avg(1)

对数字、字符串、元组等不可变类型来说,只能读取,不能更新。如果尝试重新绑定,例如count = count + 1, 会隐式创建局部变量count,这样count再不是自由变量了,也不存在闭包了。

为了解决这个问题,Python3引入了nonlocal声明。它的作用是把变量标记为自由变量。

def make_average():
count = 0
total = 0 def average():
nonlocal count, total
count += 1
total += new_value
return total / count return average

7. 实现一个简单的装饰器

例子1. 把经过的时间、传入的参数、调用的结果打印出来

#b.py
import time def clock(func): #定义内部函数clocked, 它接受任意个位置参数
def clocked(*args):
start = time.perf_counter()
#这行代码可用,因为clocked的闭包中包含自由变量func
result = func(*args)
elapsed = time.perf_counter() - start
name = func.__name__
arg_str = ', '.join(repr(arg) for arg in args)
print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
return result return clocked
#a.py
import time
from b import clock @clock
def snooze(seconds):
time.sleep(seconds) @clock
def factorial(n):
return 1 if n < 2 else n * factorial(n-1) if __name__ == '__main__':
print('*' * 40, 'Calling snooze(.123)')
snooze(.123)
print('*' * 40, 'Calling factorial(6)')
print('6! =', factorial(6))

此处clock函数的缺点:

  1. 不支持关键字参数
  2. 掩盖了被装饰函数的__name__和__doc__属性

由这个例子看出

这是装饰器的典型行为:把被装饰的函数替换成新函数,二者接受相同的参数,通常返回被装饰函数本该返回的值,同时还做些额外的操作

例子2. 解决上个例子的两个缺点

  1. 用functools.wraps装饰器把相关的属性从func复制到clocked中
  2. 此外,这个版本还能正确处理关键字参数
import time
import functools def clock(func):
@functools.wraps(func)
def clocked(*args, **kwargs):
t0 = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - t0
name = func.__name__
arg_lst = []
if args:
arg_lst.append(', '.join(repr(arg) for arg in args))
if kwargs:
pairs = ['[%0.8fs] %s(%s) -> %r ' % (elapsed, name, arg_str, result]
return result return clocked

8. 标准库中的装饰器

  1. Python内置了三个装饰器property(在19.2节讨论)、classmethod(在9.4节讨论)、staticmethod(在9.4节讨论)。

    2. 另一个常见的装饰器是functools.wraps,它的作用是协助构建行为良好的装饰器,如上面的例子。
  2. 标准库中最值得关注的是functools.lru_cache和functools.singledispatch装饰器。

8.1 functools.lru_cache()

例子1. 生成第n个斐波那契数

递归方式非常耗时,中文电子书P323

#clock为上面例子中的装饰器
from clockdeco import clock
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-2) + fibonacci(n-1) if __name__ == '__main__':
print(fibonacci(6))

例子2. 优化例子1

  1. functools.lru_cache实现了备忘录(memoization)功能的装饰器。这是一项优化技术,它把耗时的函数的结果保存起来,避免传入相同参数时重复计算。LRU代表“Least Recently Used”,表明缓存不会无限制增长,一段时间不用的条目会被扔掉。
  2. functools.lru_cache适合优化例子1这种慢速递归函数。
  3. functools.lru_cache在Web中获取信息的应用中也能发挥巨大作用。
  4. functools.lru_cache有两个可选的配置参数(maxsize=128, typed=False)
  5. functools.lru_cahe用字典存储结果,并且key根据调用时传入的位置参数和关键字参数创建,所以被lru_cache装饰的函数,它的所有参数都必须是可散列的。
import functools
from clockdeco import clock #lru_cache加()的原因是,lru_cache可以接受配置参数。
@functools.lrucache()
#这里叠放了装饰器:@lru_cache()应用到@clock返回的函数上。
@clock
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-2) + fibonacci(n-1) if __name__ == '__main__':
print(fibonacci(6))

8.2 functools.singledispatch装饰器, 单分派泛函数

  1. 中文电子书P326,根据不同的Python对象输出不同格式的HTML
  2. 问题:因为Python不支持重载方法或函数,所以我们不能使用不同的签名定义htmlize的变体。
  3. 笨拙的解决方法:把htmlize变成一个分派函数,使用一串if/elif/elif,调用专门的函数,如htmlize_str、htmlize_int等。但这样不利于模块拓展,htmlize会变得很大,而且它与各个专门函数之间的耦合也很紧密。

    解决方法2. Python3.4新增的(PyPi中的包可以向后兼容)functools.singledispatch装饰器可以把整体方案拆成多个模块。使用@singledispatch装饰的普通函数会变成泛函数(generic function):根据第一个参数的类型(一个参数为单分派,多个参数为多分派),以不同的方式执行相同操作的一组函数。
  1. Java的重载和if/elif/elif定义的分派函数的缺点是代码单元(类或函数)承担的职责过重。singledispatch的优点是支持模块化拓展,各个模块可以为它支持的各种类型注册专门函数。
from functools import singledispatch
from collections import abc
import numbers
import html # 1.singledispatch标记处理object类型的基函数。htmlize变成了泛函数
@singledispatch
def htmlize(obj):
content = html.escape(repr(obj))
return '<pre>{}</pre>'.format(content) # 2.各个专门函数用@<<base_function>>.register(<<type>>)装饰
@htmlize.register(str)
# 3.专门函数的名称无关紧要
def _(text):
content = html.escape(text).replace('\n', '<br>\n')
return '<p>{0}</p>'.format(content) # 4.numbers.Integral是int的虚拟超类
@htmlize.register(numbers.Integral)
def _(n):
return '<pre>{0} (0x{0:x})</pre>'.format(n) # 5.叠放多个register装饰器,让一个函数支持不同的类型。
@htmlize.register(tuple)
@htmlize.register(abc.MutableSequence)
def _(seq):
inner = '<li>\n</li>'.join(htmlize(item) for item in seq)
return '<ul>\n<li>' + inner + '</li>\n</ul>'

只要可能,注册的专门函数应该处理抽象基类(如numbers.Integral和abc.MutableSequence),不要处理具体实现(如int和list)。这样,代码支持的兼容类型更广泛。例如,Python拓展可以子类化numbers.Integral,使用固定的位数实现int类型。

使用抽象基类检查类型,可以让代码支持这些抽象基类现有和未来的具体子类或虚拟子类。在第11章讨论。

singledispatch提供的特性很多,查看PEP443"Single-dispatch generic functions"

9.叠加装饰器

@d1和@d2按顺序应用到f函数上,相当于f = d1(d2(f))

@d1
@d2
def f():
pass
#等同于
def f():
pass
f = d1(d2(f))

10. 参数化装饰器

  1. 解析源码中的装饰器时,Python把被装饰的函数作为第一个参数传给装饰器函数。问题:怎么让装饰器接受其他参数?
  2. 解决方案:创建一个装饰器的工厂函数,把参数传给它,返回一个装饰器,然后再把它应用到要装饰的函数上。

例子1. 给出registration.py模块的删减版,便于讲解

registry = []

def register(func):
print('running register(%s)' % func)
registry.append(func)
return func #载入模块时就运行装饰器
@register
def f1():
print('running f1()') print('running main()')
print('registry ->', registry)
print(f1)

例子2.为例子1的register添加一个可选的active参数,便于启用或禁用register执行的函数注册功能。

新的register函数不是装饰器,而是装饰器工厂函数。调用它会返回真正的装饰器,这才是应用到目标函数上的装饰器。

# 1.set对象添加和删除速度更快
registry = set()
# 2.register是一个装饰器工厂函数,返回一个装饰器,接受一个可选的关键字参数
def register(active=True):
# 3.decorate这个内部函数才是真正的装饰器;它的参数是被装饰的函数
def decorate(func):
print('running register(active=%s) -> decorate(%s)' % (active, func))
# 4.active是自由变量,从decorate的闭包中获取。只有True时才注册func
if active:
registry.add(func)
else:
# 5.如果active不为真,而且func在registry中,那么把它删除
registry.discard(func)
# 6.decorate是装饰器,返回一个函数
return func
# 7. register是工厂函数,返回decorate装饰器
return decorate # 8. @register工厂函数必须作为函数调用,并且传入可选参数
@register(active=False)
def f1():
print('running f1()') @register(active=True)
def f2():
print('running f2()') def f3():
print('running f3()') # print(registry)
# print(f1())
# print(f2())
# print(f3())

这里的关键是,register()是一个装饰器工厂函数,返回decorate装饰器,把它应用到被装饰的函数上。(思考:Flask中的URL映射也是这样?装饰器的.index()是工厂函数。一般情况下,装饰器作为函数调用,即有括号(),就是工厂函数?)

11. 参数化clock装饰器

  1. 中文电子书P334

总结:

  1. 大部分工业级的装饰器比上述所有例子都要复杂。参数化装饰器至少涉及两层嵌套函数。
  2. Graham Dumpleton和Lennart Regebro认为,装饰器最好通过实现__call__方法的类实现。作者同意使用它们建议的方式实现非平凡的装饰器, 使用函数解说这个语言特性的基本思想更容易理解。
  3. 真正理解装饰器,需要区分导入时和运行时,还要知道变量作用域、闭包和新增的nonlocal(重新绑定既不在本地作用域中也不在全局作用域中的名称)声明。
  4. 就拓展功能而言,装饰器模式比子类化更灵活。
  5. 在实现层面,Python装饰器与装饰器涉及模式不同,但有相似之处。在特定情况下,Python程序中使用函数装饰器实现装饰器模式,但是实现装饰器模式更好使用类表示装饰器和要包装的组件。

Fluent_Python_Part3函数即对象,07-closure-decoration,闭包与装饰器的更多相关文章

  1. Python虚拟机函数机制之闭包和装饰器(七)

    函数中局部变量的访问 在完成了对函数参数的剖析后,我们再来看看,在Python中,函数的局部变量时如何实现的.前面提到过,函数参数也是一种局部变量.所以,其实局部变量的实现机制与函数参数的实现机制是完 ...

  2. python之函数对象、函数嵌套、名称空间与作用域、装饰器

    一 函数对象 一 函数是第一类对象,即函数可以当作数据传递 #1 可以被引用 #2 可以当作参数传递 #3 返回值可以是函数 #3 可以当作容器类型的元素 二 利用该特性,优雅的取代多分支的if de ...

  3. python基础知识13---函数对象、函数嵌套、名称空间与作用域、装饰器

    阅读目录 一 函数对象 二 函数嵌套 三 名称空间与作用域 四 闭包函数 五 装饰器 六 练习题 一 函数对象 1 函数是第一类对象,即函数可以当作数据传递 #1 可以被引用 #2 可以当作参数传递 ...

  4. python之旅:函数对象、函数嵌套、名称空间与作用域、装饰器

    一 函数对象 一 函数是第一类对象,即函数可以当作数据传递 #1 可以被引用 #2 可以当作参数传递 #3 返回值可以是函数 #3 可以当作容器类型的元素 二 利用该特性,优雅的取代多分支的if de ...

  5. Python之路【第五篇】: 函数、闭包、装饰器、迭代器、生成器

    目录 函数补充进阶 函数对象 函数的嵌套 名称空间与作用域 闭包函数 函数之装饰器 函数之可迭代对象 函数之迭代器 函数之生成器 面向过程的程序设计思想 一.函数进阶之函数对象 1. 函数对象 秉承着 ...

  6. python 函数对象、函数嵌套、名称空间与作用域、装饰器

    一 函数对象 一 函数是第一类对象,即函数可以当作数据传递 1 可以被引用 2 可以当作参数传递 3 返回值可以是函数 3 可以当作容器类型的元素 二 利用该特性,优雅的取代多分支的if def fo ...

  7. 13、python中的函数(闭包与装饰器)

    一.嵌套函数 函数的内部又再定义另一个函数,这个函数就叫嵌套函数,里面含函数就叫内部函数. 示例: 二.返回函数 函数可以接收函数对象作为参数,同理函数也能返回一个函数对象作为返回值. 示例: 返回函 ...

  8. 一文搞懂Python函数(匿名函数、嵌套函数、闭包、装饰器)!

    Python函数定义.匿名函数.嵌套函数.闭包.装饰器 目录 Python函数定义.匿名函数.嵌套函数.闭包.装饰器 函数核心理解 1. 函数定义 2. 嵌套函数 2.1 作用 2.2 函数变量作用域 ...

  9. python_函数名的应用、闭包、装饰器

    0.动态传参内容补充: 0.1 单纯运行如下函数不会报错. def func1(*args,**kwargs): pass func1() 0.2 *的魔性用法 * 在函数定义的时候,代表聚合. *在 ...

随机推荐

  1. 松软科技课堂:jQuery 语法

    jQuery 语法 jQuery 语法是为 HTML 元素的选取编制的,可以对元素执行某些操作. 基础语法是:$(selector).action() 美元符号定义 jQuery 选择符(select ...

  2. k8s集群应用例如jenkins启动问题排查思路

    k8s集群应用例如jenkins启动问题排查思路 待办 rancher上的事件报告>pods日志>pods内容器日志(现获取容器id再查看容器日志,获取容器id 使用的是相应问题pod的名 ...

  3. Mysql中的触发器【转】

    转载:https://www.cnblogs.com/chenpi/p/5130993.html 阅读目录 什么是触发器 特点及作用 例子:创建触发器,记录表的增.删.改操作记录 弊端 什么是触发器 ...

  4. gradle-技能保存

    gradle编译java springboot,指定使用哪个环境配置文件 首先在build.gradle里面声明一个变量 def profileName = project.hasProperty(& ...

  5. koa2第一天

    router.get("/hello",async(ctx )=>{ const a=await new Promise(reslove=>reslove(123)) ...

  6. 启动docker报Failed to start Docker Application Container Engine.解决

    [root@docker ~]# systemctl status docker.service● docker.service - Docker Application Container Engi ...

  7. 2019 ICPC南京网络赛 F题 Greedy Sequence(贪心+递推)

    计蒜客题目链接:https://nanti.jisuanke.com/t/41303 题目:给你一个序列a,你可以从其中选取元素,构建n个串,每个串的长度为n,构造的si串要满足以下条件, 1. si ...

  8. css之变形(transform)

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  9. Tensorflow版本更改所产生的问题及解决方案

    1.module 'tensorflow' has no attribute 'mul' tf.mul已经在新版本中被移除,使用 tf.multiply 代替 解决方法 将tf.mul(input1, ...

  10. Educational Codeforces Round 76 (Rated for Div. 2) B. Magic Stick

    Recently Petya walked in the forest and found a magic stick. Since Petya really likes numbers, the f ...