资讯专栏INFORMATION COLUMN

流畅的python读书笔记-第七章-函数装饰器和闭包

Hydrogen / 2996人阅读

摘要:函数装饰器和闭包严格来说,装饰器只是语法糖。何时执行装饰器它们在被装饰的函数定义之后立即运行。装饰器突出了被装饰的函数的作用,还便于临时禁用某个促销策略只需把装饰器注释掉。

函数装饰器和闭包

严格来说,装饰器只是语法糖。如前所示,装饰器可以像常规的可调用
对象那样调用,其参数是另一个函数。有时,这样做更方便,尤其是做
元编程(在运行时改变程序的行为)时。

Python何时执行装饰器

它们在被装饰的函数定义之后立即运行。这通常是在导入时(即 Python 加载模块时)

registry = []

def register(func):
    print("running register(%s)" % func)
    registry.append(func)
    return func

@register
def f1():
    print("running f1()")

@register
def f2():
    print("running f2()")

def f3():
    print("running f3()")

def main():
    print("running main()")
    print("registry ->", registry)
    f1()
    f2()
    f3()

if __name__=="__main__":
    main()

把 registration.py 当作脚本运行得到的输出如下:

$ python3 registration.py
running register()
running register()
running main()
registry -> [, ]
running f1()
running f2()
running f3()
如果导入 registration.py 模块(不作为脚本运行),输出如下:
>>> import registration
running register()
running register()

此时查看 registry 的值,得到的输出如下:

>>> registration.registry
[, ]
装饰器在真实代码中的常用方式

装饰器函数与被装饰的函数在同一个模块中定义。实际情况是,装
饰器通常在一个模块中定义,然后应用到其他模块中的函数上。

使用装饰器改进“策略”模式
promos = []


def promotion(promo_func):
    promos.append(promo_func)
    return


@promotion
def fidelity(order):
    """为积分为1000或以上的顾客提供5%折扣"""
    return order.total() * .05 if order.customer.fidelity >= 1000 else 0


@promotion
def bulk_item(order):
    """单个商品为20个或以上时提供10%折扣"""
    discount = 0
    for item in order.cart:
        if item.quantity >= 20:
            discount += item.total() * .1
    return discount


@promotion
def large_order(order):
    """订单中的不同商品达到10个或以上时提供7%折扣"""
    distinct_items = {item.product for item in order.cart}
    if len(distinct_items) >= 10:
        return order.total() * .07
    return 0


def best_promo(order):
    """选择可用的最佳折扣"""
    return max(promo(order) for promo in promos)

promotion 把 promo_func 添加到 promos 列表中,然后原封不动地将其返回。

被 @promotion 装饰的函数都会添加到 promos 列表中。

与 6.1 节给出的方案相比,这个方案有几个优点。

促销策略函数无需使用特殊的名称(即不用以 _promo 结尾)。

@promotion 装饰器突出了被装饰的函数的作用,还便于临时禁用

某个促销策略:只需把装饰器注释掉。

促销折扣策略可以在其他模块中定义,在系统中的任何地方都行,只要使用 @promotion 装饰即可。

变量作用域规则

神奇的例子

>>> b = 6
>>> def f2(a):
... print(a)
... print(b)
... b = 9
...
>>> f2(3)
3
Traceback (most recent call last):
File "", line 1, in 
File "", line 3, in f2
UnboundLocalError: local variable "b" referenced before assignment
b = 9###### 它判断 b 是局部变量,因为在函数中给它赋值了

可事实是,Python 编译函数的定义体时,它判断 b 是局部变量,因为在函数中给它赋值了。

生成的字节码证实了这种判断,Python 会尝试从本地环境获取 b。

后面调用 f2(3) 时, f2 的定义体会获取并打印局部变量 a 的值,但是尝试获取局部变量 b 的值时,发现 b 没有绑定值。

为什么会这样

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

这比 JavaScript 的行为好多了,JavaScript 也不要求声明变量,但是如果忘记把变量声明为局部变量使用 var),可能会在不知情的情况下获取全局变量。

利用global就可以啦

>>> b = 6
>>> def f3(a):
... global b
... print(a)
... print(b)
... b = 9
...
>>> f3(3)
3
6
闭包

人们有时会把闭包和匿名函数弄混。
这是有历史原因的:在函数内部定义函数不常见,直到开始使用匿名函数才会这样做,

注意:

函数是不是匿名的没有关系,关键是它能访问定义体之外定义的非全局变量。

只有涉及嵌套函数时才有闭包问题。因此,很多人是同时知道这两个概念的。

案例

假如有个名为 avg 的函数,它的作用是计算不断增加的系列值的均值;

初学者可能会这样

class Averager():
    def __init__(self):
        self.series = []

    def __call__(self, new_value):
        self.series.append(new_value)
        total = sum(self.series)
        return total / len(self.series)
>>> avg = Averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0
函数式实现,使用高阶函数 make_averager。
def make_averager():
    series = []

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

    return averager
重要概念

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

审查 make_averager(见示例 7-9)创建的函数
>>> avg.__code__.co_varnames
("new_value", "total")
>>> avg.__code__.co_freevars
("series",)

series 的绑定在返回的 avg 函数的 closure 属性中。

avg.__closure__ 中的各个元素对应于avg.__code__.co_freevars 中的一个名称。

这些元素是 cell 对象,有个 cell_contents 属性,保存着真正的值。

>>> avg.__code__.co_freevars
("series",)
>>> avg.__closure__
(,)
>>> avg.__closure__[0].cell_contents
[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

问题是,当 count 是数字或任何不可变类型时

count += 1 语句的作用其实与 count = count + 1 一样。
因此,我们在 averager 的定义体中为 count 赋值了,这会把 count 变成局部变量。
注意
示例 7-9 没遇到这个问题,因为我们没有给 series 赋值,我们只是调
用 series.append,并把它传给 sum 和 len。也就是说,我们利用了
列表是可变的对象这一事实。

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

解决这个问题

为了解决这个问题,Python 3 引入了 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
对付没有 nonlocal 的 Python 2
基本上,这种处理方式是把内部函数需要修改
的变量(如 count 和 total)存储为可变对象(如字典或简单的
实例)的元素或属性,并且把那个对象绑定给一个自由变量。
实现一个简单的装饰器
import time


def clock(func):
    def clocked(*args):
        t0 = time.perf_counter()
        result = func(*args)
        elapsed = time.perf_counter() - t0
        name = func.__name__
        args_str = "".join(repr(arg) for arg in args)

        print("[%0.8fs] %s(%s) -> %r" % (elapsed, name, args_str, result))
        return result

    return clocked


@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)
    snooze(0.123)
    print("*" * 40)
    factorial(6)

    ## 这里的函数对象变成了从clocked
    print(factorial.__name__)
这是装饰器的典型行为:把被装饰的函数替换成新函数,二者接受相同的参数,而且(通

常)返回被装饰的函数本该返回的值,同时还会做些额外操作。

上面实现的 clock 装饰器有几个缺点:不支持关键字参数,而且遮盖了被装饰函
数的 namedoc 属性。

使用 functools.wraps 装饰器把相关的属性从 func 复制到 clocked 中。此外,这个新版还能正确处理关键字参数。
import time
import functools


def clock(func):
    @functools.wraps(func)  ###这里 保留__name__ 和 __doc__ 属性
    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 = ["%s=%r" % (k, w) for k, w in sorted(kwargs.items())]
        arg_lst.append(", ".join(pairs))
        arg_str = ", ".join(arg_lst)
        print("[%0.8fs] %s(%s) -> %r " % (elapsed, name, arg_str, result))
        return result

    return clocked
标准库中的装饰器

functools.lru_cache 是非常实用的装饰器,它实现了备忘(memoization)功能。
就是更加利用缓存干活

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 = ["%s=%r" % (k, w) for k, w in sorted(kwargs.items())]
        arg_lst.append(", ".join(pairs))
        arg_str = ", ".join(arg_lst)
        print("[%0.8fs] %s(%s) -> %r " % (elapsed, name, arg_str, result))
        return result

    return clocked





@clock
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 2) + fibonacci(n - 1)


if __name__ == "__main__":
    print(fibonacci(6))
浪费时间的地方很明显:fibonacci(1) 调用了 8 次,fibonacci(2) 调用了 5 次……但是,如果增加两行代码,使用 lru_cache,性能会显著改善,
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 = ["%s=%r" % (k, w) for k, w in sorted(kwargs.items())]
        arg_lst.append(", ".join(pairs))
        arg_str = ", ".join(arg_lst)
        print("[%0.8fs] %s(%s) -> %r " % (elapsed, name, arg_str, result))
        return result

    return clocked


@functools.lru_cache()  # 
@clock  #
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 2) + fibonacci(n - 1)


if __name__ == "__main__":
    print(fibonacci(6))
❶ 注意,必须像常规函数那样调用 lru_cache。这一行中
有一对括
号:@functools.lru_cache()。这么做的原因是,lru_cache 可以接受配置参数,稍
后说明。
lru_cache 可以使用两个可选的参数来配置。
functools.lru_cache(maxsize=128, typed=False)

maxsize 参数指定存储多少个调用的结果。

缓存满了之后,旧的结果会被扔掉,腾出空间。

为了得到最佳性能,maxsize 应该设为 2 的幂。typed 参数如果设为 True,把不同

参数类型得到的结果分开保存,即把通常认为相等的浮点数和整数参数(如 1 和 1.0)区

分开。

functools.singledispatch 装饰器 让Python强行支持重载方法
因为 Python 不支持重载方法或函数,所以我们不能使用不同的签名定义 htmlize 的变体,也无法使用不同的方式处理不同的数据类型。
使用 @singledispatch 装饰的普通函数会变成泛函数(generic function):根据第一个参数的类型,以不同方式执行相同操作的一组函数。 让Python强行支持重载方法,不再使用一串 if/elif/elif,调用专门的函数
singledispatch 创建一个自定义的 htmlize.register 装饰器,把多

个函数绑在一起组成一个泛函数

from functools import singledispatch
from collections import abc
import numbers
import html


@singledispatch
def htmlize(obj):
    content = html.escape(repr(obj))
    return "
{}
".format(content) @htmlize.register(str) def _(text): content = html.escape(text).replace(" ", "
") return "

{0}

".format(content) @htmlize.register(numbers.Integral) def _(n): return "
{0} (0x{0:x})
".format(n) @htmlize.register(tuple) @htmlize.register(abc.MutableSequence) def _(seq): inner = "
  • ".join(htmlize(item) for item in seq) return "
    • " + inner + "
    " print(htmlize({1, 2, 3})) print(htmlize(abs)) print(htmlize("Heimlich & Co. - a game")) print(htmlize(42)) ##这个强啊!!! print(htmlize(["alpha", 66, {3, 2, 1}]))
  • ❷ 各个专门函数使用 @«base_function».register(«type») 装饰。
    ❸ 专门函数的名称无关紧要;_ 是个不错的选择,简单明了。
    为每个需要特殊处理的类型注册一个函数。numbers.Integral 是 int 的虚拟超类。
    ❺ 可以叠放多个 register 装饰器,让同一个函数支持不同类型。

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

    例如,Python 扩展可以子类化 numbers.Integral,使用固定的位数实
    现 int 类型。
    注意:

    @singledispatch 不是为了把 Java 的那种方法重载带入 Python。在一个类中 为同一个方法定义多个重载变体,

    @singledispath 的优点是支持模块化扩展:各个模块可以为它支持的各个类型注册一个专门函数。

    叠放装饰器
    @d1
    @d2
    def f():
        print("f")

    等同于

    def f():
        print("f")
    f = d1(d2(f))
    一个参数化的注册装饰器

    为了便于启用或禁用 register 执行的函数注册功能,我们为它提供一个可选的 active
    参数,设为 False 时,不注册被装饰的函数。

    registry = set()
    
    
    def register(active=True):
        def decorate(func):
            print("running register(active=%s)->decorate(%s)"
                  % (active, func))
            if active:
                registry.add(func)
            else:
                registry.discard(func)
                return func
    
        return decorate
    
    
    @register(active=False)
    def f1():
        print("running f1()")
    
    @register()
    def f2():
        print("running f2()")
    
    def f3():
        print("running f3()")
    
    if __name__ =="__main__":
        print(registry)
    参数化装饰器通常会把被装饰的函数替换掉,而且结构上需要多一层嵌套。
    参数化clock装饰器
    import time
    
    DEFAULT_FMT = "花费时间:[{elapsed:0.5f}s] 程序名:{name}  参数:({args}) -> 结果:{result}"
    
    
    def clock(fmt=DEFAULT_FMT):
        def decorate(func):
            def clocked(*_args):
                t0 = time.time()
                _result = func(*_args)
    
                ### locals() 局部变量
                elapsed = time.time() - t0
                name = func.__name__
                args = ", ".join(repr(arg) for arg in _args)
                result = repr(_result)
                    # 这里不知道他为什么这么能用
                print(fmt.format(**locals()))
    
                return _result
    
            return clocked
    
        return decorate
    
    
    if __name__ == "__main__":
    
        # ## 第一种情况
        # @clock()
        # def snooze(seconds):
        #     time.sleep(seconds)
    
    
        ## 第二种情况
        # @clock("程序名:{name}: 花费时间:{elapsed}s")
        # def snooze(seconds):
        #     time.sleep(seconds)
    
        ## 第三种情况
        @clock("程序名:{name}  参数:({args})  花费时间:dt={elapsed:0.3f}s")
        def snooze(seconds):
            time.sleep(seconds)
    
    
    
        snooze(0.123)
    
    
    

    clock 是参数化装饰器工厂函数
    ❷ decorate 是真正的装饰器。
    ❸ clocked 包装被装饰的函数。
    ❹ _result 是被装饰的函数返回的真正结果

    这里的locals()是啥不知道
    def runnoob(arg:"int"):
        z = 1
        print(arg + 1)
    
        # 返回字典类型的局部变量。
        print("==="*30)
        print(locals())
    
        # 返回字典类型的全部变量。
        print("=" * 50)
        print(globals())
    
    
    
    num = 8
    runnoob(num)
    
    小总结

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

    它们在被装饰的函数定义之后立即运行。这通常是在导入时(即 Python 加载模块时)

    装饰器改进了策略模式

    闭包

    闭包是一种函数,它会保留定义函数时存在的自由变量的绑定

    Python 3 引入了 nonlocal 声明。它的作用是把变

    量标记为自由变量

    functools.wraps 装饰器把相关的属性从 func 复制到 clocked 中。此外,这个新版还能正确处理关键字参数

    functools.lru_cache 是非常实用的装饰器,它实现了备忘(memoization)功能。就是更加利用缓存干活 functools.singledispatch 装饰器 让Python强行支持重载方法 locals() globals()

    *locals

    ** locals()

    文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

    转载请注明本文地址:https://www.ucloud.cn/yun/41655.html

    相关文章

    • Python学习之路26-函数装饰器和闭包

      摘要:初步认识装饰器函数装饰器用于在源代码中标记函数,以某种方式增强函数的行为。函数装饰器在导入模块时立即执行,而被装饰的函数只在明确调用时运行。只有涉及嵌套函数时才有闭包问题。如果想保留函数原本的属性,可以使用标准库中的装饰器。 《流畅的Python》笔记本篇将从最简单的装饰器开始,逐渐深入到闭包的概念,然后实现参数化装饰器,最后介绍标准库中常用的装饰器。 1. 初步认识装饰器 函数装饰...

      sunny5541 评论0 收藏0
    • 流畅python读书笔记-第十四章-可迭代对象、迭代器和生成器

      摘要:可迭代的对象迭代器和生成器理念迭代是数据处理的基石。可迭代的对象与迭代器的对比从可迭代的对象中获取迭代器标准的迭代器接口有两个方法。此外,也没有办法还原迭代器。最终,函数的定义体返回时,外层的生成器对象会抛出异常这一点与迭代器协议一致。 可迭代的对象、迭代器和生成器 理念 迭代是数据处理的基石。扫描内存中放不下的数据集时,我们要找到一种惰性获取数据项的方式,即按需一次获取一个数据项。这...

      kohoh_ 评论0 收藏0
    • Python函数装饰器和闭包

      摘要:变量查找规则在中一个变量的查找顺序是局部环境,闭包,全局,内建闭包引用了自由变量的函数。闭包的作用闭包的最大特点是可以将父函数的变量与内部函数绑定,并返回绑定变量后的函数,此时即便生成闭包的环境父函数已经释放,闭包仍然存在。 导语:本文章记录了本人在学习Python基础之函数篇的重点知识及个人心得,打算入门Python的朋友们可以来一起学习并交流。 本文重点: 1、掌握装饰器的本质、功...

      caozhijian 评论0 收藏0
    • Python基础教程》第六章--读书笔记

      摘要:第六章抽象本章会介绍如何将语句组织成函数。关键字参数和默认值目前为止,我们使用的参数都是位置参数,因为它们的位置很重要,事实上比它们的名字更重要。参数前的星号将所有值放置在同一个元祖中。函数内的变量被称为局部变量。 第六章:抽象 本章会介绍如何将语句组织成函数。还会详细介绍参数(parameter)和作用域(scope)的概念,以及递归的概念及其在程序中的用途。 懒惰即美德 斐波那契数...

      AnthonyHan 评论0 收藏0

    发表评论

    0条评论

    最新活动
    阅读需要支付1元查看
    <