Fluent Python - Part7 函数装饰器和闭包

装饰器基础知识

  • 装饰器是一个可调用对象,其参数是另一个函数
  • 装饰器可能会处理被装饰的函数,然后把它返回,或者将其替换成另一个函数或者可调用对象。

一个例子

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

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

target()
print(target)

"""
output:
running inner()
<function deco.<locals>.inner at 0x103fda550>
"""

Python 何时执行装饰器

  • 装饰器的一个关键特性是,它们在被装饰的函数定义之后立即运行。即在模块被导入时就运行了。

变量作用域规则

首先看一个例子

>>> b = 6
>>> def f2(a):
...     print(a)
...     print(b)
...     b = 9
...
>>> f2(3)
3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in f2
UnboundLocalError: local variable 'b' referenced before assignment

奇怪的地方在这里,当执行到print(b)语句时,明明有一个全局变量b,但仍然没有输出它,而是直接抛出异常。造成这种现象的原因是,Python 编译函数的定义体时,它判断 b 是局部变量,因为在函数中给它赋值了。当运行到 print(b) 时,Python 会尝试从本地环境获取 b。但是尝试获取局部变量 b 的值时,发现 b 没有绑定值。

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

如果在函数中赋值时想让解释器把 b 当成全局变量,要使用 global 声明.

>>> b = 6
>>> def f2(a):
...     global b
...     print(a)
...     print(b)
...     b = 9
...
>>> f2(3)
3
6

闭包

闭包指延伸了作用域的函数,其中包含了定义体中引用,但是不在定义体中定义的非全局变量。函数是不是匿名的没有关系,关键是它能访问定义体之外定义的非全局变量。

一个例子

def make_average():
    series = []

    def average(new_value):
        series.append(new_value)
        total = sum(series)
        return total/len(series)
    
    return average

avg = make_average()
print(avg(10))
print(avg(11))
print(avg(12))

"""
output:
10.0
10.5
11.0
"""

seriesmake_average 函数的局部变量,因为那个函数的定义体中初始化了 series: series = [].可是,调用 avg(10) 时, make_average 函数已经返回了,而它的本地作用域也一去不复返了。

average 函数中,series 是自由变量(free variable)。 这是一个技术术语,指未在本地作用域中绑定的变量。

闭包

print(avg.__code__.co_varnames)
print(avg.__code__.co_freevars)
"""
output
('new_value', 'total')
('series',)
"""

series 的绑定在返回的 avg 函数中的 __closure__ 属性中。 avg.__closure__ 中的各个元素对应于 avg.__code__.co_freevars 中的一个名称。这些元素是 cell 对象,cell 对象有个 cell_contents 属性,保存着真正的值。这些属性的值如下所示:

print(avg.__closure__)
print(avg.__closure__[0].cell_contents)
"""
output:
(<cell at 0x108ebf160: list object at 0x108eb3c00>,)
[10, 11, 12]
"""

nonlocal 声明

一个例子

def make_averager():
    count = 0
    total = 0

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

    return averager

avg = make_averager()
print(avg(10))
"""
output:
Traceback (most recent call last):
  File "/Users/cguo/conan/b.py", line 13, in <module>
    print(avg(10))
  File "/Users/cguo/conan/b.py", line 6, in averager
    count += 1
UnboundLocalError: local variable 'count' referenced before assignment
"""

问题是,当 count 是数字或任何不可变类型时, count += 1 语句的作用其实与 count = count + 1 一样。因此,我们在 averager 的定义体中为 count 赋值了,这会把 count 变成局部变量。
为了解决这个问题, Python3 引入了 nonlocal 声明。 它的作用是把变量标记为自由变量, 即使在函数中为变脸赋予新值了,也会变成自由变量。如果为 nonlocal 声明的变量赋予新值,闭包中保存的绑定会更新。

def make_averager():
    count = 0
    total = 0

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

    return averager

avg = make_averager()
print(avg(10))
  • python2 怎么办呢?我们其实可以比较trick的实现,就是将这些变量存储到可变对象当中(如字典或简单的实例)的元素或属性当中,然后把这个对象绑定给一个自由变量。

标准库中的装饰器

使用 functools.lru_cache 做备忘

functools.lru_cache 是非常实用的装饰器,它实现了备忘功能。它有两个可选参数来配置,它的签名是:

functools.lru_cache(maxsize=128, typed=False)

maxsize 参数指定存储多少个调用的结果。缓存满了之后,旧的结果会被扔掉,腾出空间。为了得到最佳性能,maxsize 应该设为2的幂。 typed 参数如果设为 True, 把不同参数类型得到的结果分开保存,即把通常认为相等的浮点数和整数参数(如 1 和 1.0) 区分开。另外,因为 lru_cache 实用字典存储结果,而且键根据调用时传入的定位参数和关键字参数创建,所以被 lru_cache 装饰的函数,它的所有参数都必须是可散列的。

单分派泛函数

Python3.4 新增的 functools.singledispatch 装饰器可以把整体方案拆分成多个模块,甚至可以为你无法修改的类提供专门的函数。使用 @singledispatch 装饰的普通函数会变成泛函数:根据第一个参数的类型,以不同方式执行相同操作的一组函数。

一个例子

from functools import singledispatch
from collections import abc
import numbers

@singledispatch
def Print(obj):
    print("hello obj")

@Print.register(str)
def _(text):
    print("{} is str".format(text))

@Print.register(numbers.Integral)
def _(n):
    print("{} is number".format(n))

@Print.register(tuple)
def _(seq):
    print("{} is tuple".format(seq))

Print([1,2,3])
Print(1)
Print("1")
Print((1,2,3))

"""
output:
hello obj
1 is number
1 is str
(1, 2, 3) is tuple
"""

参数化装饰器

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

一个例子

def my_decorator(word):
    def decorate(func):
        print(word)
        def f(*args, **kwargs):
            return func(*args, **kwargs)
        return f
    return decorate



@my_decorator("balabala")
def f1():
    print("hello world")

f1()
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值