流畅的 Python 第二版(GPT 重译)(五)(2)

简介: 流畅的 Python 第二版(GPT 重译)(五)

流畅的 Python 第二版(GPT 重译)(五)(1)https://developer.aliyun.com/article/1484580

如果尝试 示例 9-12,你会得到以下结果:

>>> avg = make_averager()
>>> avg(10)
Traceback (most recent call last):
  ...
UnboundLocalError: local variable 'count' referenced before assignment
>>>

问题在于语句 count += 1 实际上意味着与 count = count + 1 相同,当 count 是一个数字或任何不可变类型时。因此,我们实际上是在 averager 的主体中对 count 赋值,这使其成为一个局部变量。同样的问题也影响到 total 变量。

我们在 示例 9-8 中没有这个问题,因为我们从未给 series 赋值;我们只调用了 series.append 并在其上调用了 sumlen。所以我们利用了列表是可变的这一事实。

但对于像数字、字符串、元组等不可变类型,你只能读取,而不能更新。如果尝试重新绑定它们,比如 count = count + 1,那么实际上是隐式创建了一个局部变量 count。它不再是一个自由变量,因此不会保存在闭包中。

为了解决这个问题,Python 3 中引入了 nonlocal 关键字。它允许你将一个变量声明为自由变量,即使它在函数内部被赋值。如果向 nonlocal 变量赋予新值,闭包中存储的绑定将会改变。我们最新的 make_averager 的正确实现看起来像 示例 9-13。

示例 9-13. 计算不保留所有历史记录的运行平均值(使用 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 的变量查找工作原理。

变量查找逻辑

当定义一个函数时,Python 字节码编译器根据以下规则确定如何获取其中出现的变量 x:³

  • 如果有 global x 声明,x 来自并被赋值给模块的 x 全局变量。⁴
  • 如果有 nonlocal x 声明,x 来自并被赋值给最近的周围函数的 x 局部变量,其中 x 被定义。
  • 如果 x 是参数或在函数体中被赋值,则 x 是局部变量。
  • 如果引用了x但未被赋值且不是参数:
  • x 将在周围函数体的本地作用域(非本地作用域)中查找。
  • 如果在周围作用域中找不到,将从模块全局作用域中读取。
  • 如果在全局作用域中找不到,将从 __builtins__.__dict__ 中读取。

现在我们已经介绍了 Python 闭包,我们可以有效地使用嵌套函数实现装饰器。

实现一个简单的装饰器

示例 9-14 是一个装饰器,用于记录装饰函数的每次调用并显示经过的时间、传递的参数以及调用的结果。

示例 9-14. clockdeco0.py: 显示函数运行时间的简单装饰器
import time
def clock(func):
    def clocked(*args):  # ①
        t0 = time.perf_counter()
        result = func(*args)  # ②
        elapsed = time.perf_counter() - t0
        name = func.__name__
        arg_str = ', '.join(repr(arg) for arg in args)
        print(f'[{elapsed:0.8f}s] {name}({arg_str}) -> {result!r}')
        return result
    return clocked  # ③

定义内部函数 clocked 来接受任意数量的位置参数。

这行代码之所以有效,是因为 clocked 的闭包包含了 func 自由变量。

返回内部函数以替换装饰的函数。

示例 9-15 演示了 clock 装饰器的使用。

示例 9-15. 使用 clock 装饰器
import time
from clockdeco0 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))

运行 示例 9-15 的输出如下所示:

$ python3 clockdeco_demo.py
**************************************** Calling snooze(.123)
[0.12363791s] snooze(0.123) -> None
**************************************** Calling factorial(6)
[0.00000095s] factorial(1) -> 1
[0.00002408s] factorial(2) -> 2
[0.00003934s] factorial(3) -> 6
[0.00005221s] factorial(4) -> 24
[0.00006390s] factorial(5) -> 120
[0.00008297s] factorial(6) -> 720
6! = 720

工作原理

记住这段代码:

@clock
def factorial(n):
    return 1 if n < 2 else n*factorial(n-1)

实际上是这样的:

def factorial(n):
    return 1 if n < 2 else n*factorial(n-1)
factorial = clock(factorial)

因此,在这两个示例中,clockfactorial函数作为其func参数(参见示例 9-14)。然后创建并返回clocked函数,Python 解释器将其分配给factorial(在第一个示例中,在幕后)。实际上,如果导入clockdeco_demo模块并检查factorial__name__,您会得到以下结果:

>>> import clockdeco_demo
>>> clockdeco_demo.factorial.__name__
'clocked'
>>>

所以factorial现在实际上持有对clocked函数的引用。从现在开始,每次调用factorial(n),都会执行clocked(n)。实质上,clocked执行以下操作:

  1. 记录初始时间t0
  2. 调用原始factorial函数,保存结果。
  3. 计算经过的时间。
  4. 格式化并显示收集的数据。
  5. 返回第 2 步保存的结果。

这是装饰器的典型行为:它用新函数替换装饰函数,新函数接受相同的参数并(通常)返回装饰函数应该返回的内容,同时还进行一些额外处理。

提示

在 Gamma 等人的设计模式中,装饰器模式的简短描述以“动态地为对象附加额外的责任”开始。函数装饰器符合这一描述。但在实现层面上,Python 装饰器与原始设计模式作品中描述的经典装饰器几乎没有相似之处。“讲台”有更多相关内容。

示例 9-14 中实现的clock装饰器存在一些缺陷:它不支持关键字参数,并且掩盖了装饰函数的__name____doc__。示例 9-16 使用functools.wraps装饰器从func复制相关属性到clocked。此外,在这个新版本中,关键字参数被正确处理。

示例 9-16. clockdeco.py:改进的时钟装饰器
import time
import functools
def clock(func):
    @functools.wraps(func)
    def clocked(*args, **kwargs):
        t0 = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - t0
        name = func.__name__
        arg_lst = [repr(arg) for arg in args]
        arg_lst.extend(f'{k}={v!r}' for k, v in kwargs.items())
        arg_str = ', '.join(arg_lst)
        print(f'[{elapsed:0.8f}s] {name}({arg_str}) -> {result!r}')
        return result
    return clocked

functools.wraps只是标准库中可用的装饰器之一。在下一节中,我们将介绍functools提供的最令人印象深刻的装饰器:cache

标准库中的装饰器

Python 有三个内置函数专门用于装饰方法:propertyclassmethodstaticmethod。我们将在“使用属性进行属性验证”中讨论property,并在“classmethod 与 staticmethod”中讨论其他内容。

在示例 9-16 中,我们看到另一个重要的装饰器:functools.wraps,一个用于构建行为良好的装饰器的辅助工具。标准库中一些最有趣的装饰器是cachelru_cachesingledispatch,它们都来自functools模块。我们将在下一节中介绍它们。

使用functools.cache进行记忆化

functools.cache装饰器实现了记忆化:⁵一种通过保存先前调用昂贵函数的结果来优化的技术,避免对先前使用的参数进行重复计算。

提示

functools.cache在 Python 3.9 中添加。如果需要在 Python 3.8 中运行这些示例,请将@cache替换为@lru_cache。对于 Python 的早期版本,您必须调用装饰器,写成@lru_cache(),如“使用 lru_cache”中所述。

一个很好的演示是将@cache应用于痛苦缓慢的递归函数,以生成斐波那契数列中的第n个数字,如示例 9-17 所示。

示例 9-17. 计算斐波那契数列中第 n 个数字的非常昂贵的递归方式
from clockdeco import clock
@clock
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 2) + fibonacci(n - 1)
if __name__ == '__main__':
    print(fibonacci(6))

运行fibo_demo.py的结果如下。除了最后一行外,所有输出都是由clock装饰器生成的:

$ python3 fibo_demo.py
[0.00000042s] fibonacci(0) -> 0
[0.00000049s] fibonacci(1) -> 1
[0.00006115s] fibonacci(2) -> 1
[0.00000031s] fibonacci(1) -> 1
[0.00000035s] fibonacci(0) -> 0
[0.00000030s] fibonacci(1) -> 1
[0.00001084s] fibonacci(2) -> 1
[0.00002074s] fibonacci(3) -> 2
[0.00009189s] fibonacci(4) -> 3
[0.00000029s] fibonacci(1) -> 1
[0.00000027s] fibonacci(0) -> 0
[0.00000029s] fibonacci(1) -> 1
[0.00000959s] fibonacci(2) -> 1
[0.00001905s] fibonacci(3) -> 2
[0.00000026s] fibonacci(0) -> 0
[0.00000029s] fibonacci(1) -> 1
[0.00000997s] fibonacci(2) -> 1
[0.00000028s] fibonacci(1) -> 1
[0.00000030s] fibonacci(0) -> 0
[0.00000031s] fibonacci(1) -> 1
[0.00001019s] fibonacci(2) -> 1
[0.00001967s] fibonacci(3) -> 2
[0.00003876s] fibonacci(4) -> 3
[0.00006670s] fibonacci(5) -> 5
[0.00016852s] fibonacci(6) -> 8
8

浪费是显而易见的:fibonacci(1)被调用了八次,fibonacci(2)被调用了五次,等等。但只需添加两行代码来使用cache,性能就得到了很大改善。参见示例 9-18。

示例 9-18. 使用缓存实现更快的方法
import functools
from clockdeco import clock
@functools.cache  # ①
@clock  # ②
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 2) + fibonacci(n - 1)
if __name__ == '__main__':
    print(fibonacci(6))

这行代码适用于 Python 3.9 或更高版本。有关支持较早 Python 版本的替代方法,请参阅“使用 lru_cache”。

这是堆叠装饰器的一个示例:@cache应用于@clock返回的函数。

堆叠装饰器

要理解堆叠装饰器的意义,回想一下@是将装饰器函数应用于其下方的函数的语法糖。如果有多个装饰器,它们的行为类似于嵌套函数调用。这个:

@alpha
@beta
def my_fn():
    ...

与此相同:

my_fn = alpha(beta(my_fn))

换句话说,首先应用beta装饰器,然后将其返回的函数传递给alpha

在示例 9-18 中使用cachefibonacci函数仅对每个n值调用一次:

$ python3 fibo_demo_lru.py
[0.00000043s] fibonacci(0) -> 0
[0.00000054s] fibonacci(1) -> 1
[0.00006179s] fibonacci(2) -> 1
[0.00000070s] fibonacci(3) -> 2
[0.00007366s] fibonacci(4) -> 3
[0.00000057s] fibonacci(5) -> 5
[0.00008479s] fibonacci(6) -> 8
8

在另一个测试中,计算fibonacci(30)时,示例 9-18 在 0.00017 秒内完成了所需的 31 次调用(总时间),而未缓存的示例 9-17 在 Intel Core i7 笔记本上花费了 12.09 秒,因为它调用了fibonacci(1)832,040 次,总共 2,692,537 次调用。

被装饰函数接受的所有参数必须是可散列的,因为底层的lru_cache使用dict来存储结果,键是由调用中使用的位置和关键字参数生成的。

除了使愚蠢的递归算法可行外,@cache在需要从远程 API 获取信息的应用程序中表现出色。

警告

如果缓存条目数量非常大,functools.cache可能会消耗所有可用内存。我认为它更适合用于短暂的命令本。在长时间运行的进程中,我建议使用适当的maxsize参数使用functools.lru_cache,如下一节所述。

使用 lru_cache

functools.cache装饰器实际上是围绕旧的functools.lru_cache函数的简单包装器,后者更灵活,与 Python 3.8 及更早版本兼容。

@lru_cache的主要优势在于其内存使用受maxsize参数限制,其默认值相当保守,为 128,这意味着缓存最多同时保留 128 个条目。

LRU 的首字母缩写代表最近最少使用,意味着长时间未被读取的旧条目将被丢弃,以腾出空间给新条目。

自 Python 3.8 以来,lru_cache可以以两种方式应用。这是最简单的使用方法:

@lru_cache
def costly_function(a, b):
    ...

另一种方式——自 Python 3.2 起可用——是将其作为函数调用,使用()

@lru_cache()
def costly_function(a, b):
    ...

在这两种情况下,将使用默认参数。这些是:

maxsize=128

设置要存储的条目的最大数量。缓存满后,最近最少使用的条目将被丢弃以为新条目腾出空间。为了获得最佳性能,maxsize应为 2 的幂。如果传递maxsize=None,LRU 逻辑将被禁用,因此缓存工作速度更快,但条目永远不会被丢弃,这可能会消耗过多内存。这就是@functools.cache的作用。

typed=False

确定不同参数类型的结果是否分开存储。例如,在默认设置中,被视为相等的浮点数和整数参数仅存储一次,因此对f(1)f(1.0)的调用将只有一个条目。如果typed=True,这些参数将产生不同的条目,可能存储不同的结果。

这是使用非默认参数调用@lru_cache的示例:

@lru_cache(maxsize=2**20, typed=True)
def costly_function(a, b):
    ...

现在让我们研究另一个强大的装饰器:functools.singledispatch

单分发通用函数

想象我们正在创建一个用于调试 Web 应用程序的工具。我们希望为不同类型的 Python 对象生成 HTML 显示。

我们可以从这样的函数开始:

import html
def htmlize(obj):
    content = html.escape(repr(obj))
    return f'<pre>{content}</pre>'

这将适用于任何 Python 类型,但现在我们想扩展它以生成一些类型的自定义显示。一些示例:

str

'
\n'
替换嵌入的换行符,并使用

标签代替

int

以十进制和十六进制形式显示数字(对 bool 有特殊情况)。

list

输出一个 HTML 列表,根据其类型格式化每个项目。

floatDecimal

通常输出值,但也以分数形式呈现(为什么不呢?)。

我们想要的行为在 Example 9-19 中展示。

Example 9-19. htmlize() 生成针对不同对象类型定制的 HTML
>>> htmlize({1, 2, 3})  # ①
'<pre>{1, 2, 3}</pre>' >>> htmlize(abs)
'<pre>&lt;built-in function abs&gt;</pre>' >>> htmlize('Heimlich & Co.\n- a game')  # ②
'<p>Heimlich &amp; Co.<br/>\n- a game</p>' >>> htmlize(42)  # ③
'<pre>42 (0x2a)</pre>' >>> print(htmlize(['alpha', 66, {3, 2, 1}]))  # ④
<ul> <li><p>alpha</p></li> <li><pre>66 (0x42)</pre></li> <li><pre>{1, 2, 3}</pre></li> </ul> >>> htmlize(True)  # ⑤
'<pre>True</pre>' >>> htmlize(fractions.Fraction(2, 3))  # ⑥
'<pre>2/3</pre>' >>> htmlize(2/3)   # ⑦
'<pre>0.6666666666666666 (2/3)</pre>' >>> htmlize(decimal.Decimal('0.02380952'))
'<pre>0.02380952 (1/42)</pre>'

原始函数为 object 注册,因此它作为一个通用函数来处理与其他实现不匹配的参数类型。

str 对象也会进行 HTML 转义,但会被包裹在

中,并在每个 '\n' 前插入
换行符。

int 以十进制和十六进制的形式显示,在

 
    

中。

每个列表项根据其类型进行格式化,并将整个序列呈现为 HTML 列表。

尽管 boolint 的子类型,但它得到了特殊处理。

以分数形式展示 Fraction

以近似分数等价形式展示 floatDecimal

函数 singledispatch

因为在 Python 中我们没有 Java 风格的方法重载,所以我们不能简单地为我们想要以不同方式处理的每种数据类型创建 htmlize 的变体。在 Python 中的一个可能的解决方案是将 htmlize 转变为一个分发函数,使用一系列的 if/elif/…match/case/… 调用专门函数,如 htmlize_strhtmlize_int 等。这种方法对我们模块的用户来说是不可扩展的,而且很笨重:随着时间的推移,htmlize 分发器会变得太大,它与专门函数之间的耦合会非常紧密。

functools.singledispatch 装饰器允许不同模块为整体解决方案做出贡献,并让您轻松为属于第三方包的类型提供专门函数,而这些包您无法编辑。如果您用 @singledispatch 装饰一个普通函数,它将成为一个通用函数的入口点:一组函数以不同方式执行相同操作,取决于第一个参数的类型。这就是所谓的单分派。如果使用更多参数来选择特定函数,我们将有多分派。Example 9-20 展示了如何实现。

警告

functools.singledispatch 自 Python 3.4 起存在,但自 Python 3.7 起才支持类型提示。Example 9-20 中的最后两个函数展示了在 Python 3.4 以来所有版本中都有效的语法。

Example 9-20. @singledispatch 创建一个自定义的 @htmlize.register 来将几个函数捆绑成一个通用函数
from functools import singledispatch
from collections import abc
import fractions
import decimal
import html
import numbers
@singledispatch  # ①
def htmlize(obj: object) -> str:
    content = html.escape(repr(obj))
    return f'<pre>{content}</pre>'
@htmlize.register  # ②
def _(text: str) -> str:  # ③
    content = html.escape(text).replace('\n', '<br/>\n')
    return f'<p>{content}</p>'
@htmlize.register  # ④
def _(seq: abc.Sequence) -> str:
    inner = '</li>\n<li>'.join(htmlize(item) for item in seq)
    return '<ul>\n<li>' + inner + '</li>\n</ul>'
@htmlize.register  # ⑤
def _(n: numbers.Integral) -> str:
    return f'<pre>{n} (0x{n:x})</pre>'
@htmlize.register  # ⑥
def _(n: bool) -> str:
    return f'<pre>{n}</pre>'
@htmlize.register(fractions.Fraction)  # ⑦
def _(x) -> str:
    frac = fractions.Fraction(x)
    return f'<pre>{frac.numerator}/{frac.denominator}</pre>'
@htmlize.register(decimal.Decimal)  # ⑧
@htmlize.register(float)
def _(x) -> str:
    frac = fractions.Fraction(x).limit_denominator()
    return f'<pre>{x} ({frac.numerator}/{frac.denominator})</pre>'

@singledispatch 标记了处理 object 类型的基本函数。

每个专门函数都使用 @«base».register 进行装饰。

运行时给定的第一个参数的类型决定了何时使用这个特定的函数定义。专门函数的名称并不重要;_ 是一个很好的选择,可以让这一点清晰明了。⁶

为了让每个额外的类型得到特殊处理,需要注册一个新的函数,并在第一个参数中使用匹配的类型提示。

numbers ABCs 对于与 singledispatch 一起使用很有用。⁷

boolnumbers.Integral子类型,但singledispatch逻辑寻找具有最具体匹配类型的实现,而不考虑它们在代码中出现的顺序。

如果您不想或无法向装饰的函数添加类型提示,可以将类型传递给@«base».register装饰器。这种语法适用于 Python 3.4 或更高版本。

@«base».register装饰器返回未装饰的函数,因此可以堆叠它们以在同一实现上注册两个或更多类型。⁸

在可能的情况下,注册专门的函数以处理抽象类(ABCs)如numbers.Integralabc.MutableSequence,而不是具体实现如intlist。这样可以使您的代码支持更多兼容类型的变化。例如,Python 扩展可以提供numbers.Integral的子类作为int类型的替代方案。

流畅的 Python 第二版(GPT 重译)(五)(3)https://developer.aliyun.com/article/1484583

相关文章
|
1天前
|
数据采集 存储 人工智能
【Python+微信】【企业微信开发入坑指北】4. 企业微信接入GPT,只需一个URL,自动获取文章总结
【Python+微信】【企业微信开发入坑指北】4. 企业微信接入GPT,只需一个URL,自动获取文章总结
40 0
|
1天前
|
Python
过年了,让GPT用Python给你写个放烟花的程序吧!
过年了,让GPT用Python给你写个放烟花的程序吧!
20 0
|
1天前
|
人工智能 JSON 机器人
【Chat GPT】用 ChatGPT 运行 Python
【Chat GPT】用 ChatGPT 运行 Python
|
1天前
|
机器学习/深度学习 人工智能 自然语言处理
总结几个GPT的超实用之处【附带Python案例】
总结几个GPT的超实用之处【附带Python案例】
|
1天前
|
前端开发 JavaScript 安全
JavaScript 权威指南第七版(GPT 重译)(七)(4)
JavaScript 权威指南第七版(GPT 重译)(七)
29 0
|
1天前
|
前端开发 JavaScript 算法
JavaScript 权威指南第七版(GPT 重译)(七)(3)
JavaScript 权威指南第七版(GPT 重译)(七)
38 0
|
1天前
|
前端开发 JavaScript Unix
JavaScript 权威指南第七版(GPT 重译)(七)(2)
JavaScript 权威指南第七版(GPT 重译)(七)
43 0
|
1天前
|
前端开发 JavaScript 算法
JavaScript 权威指南第七版(GPT 重译)(七)(1)
JavaScript 权威指南第七版(GPT 重译)(七)
67 0
|
1天前
|
存储 前端开发 JavaScript
JavaScript 权威指南第七版(GPT 重译)(六)(4)
JavaScript 权威指南第七版(GPT 重译)(六)
119 3
JavaScript 权威指南第七版(GPT 重译)(六)(4)
|
1天前
|
前端开发 JavaScript API
JavaScript 权威指南第七版(GPT 重译)(六)(3)
JavaScript 权威指南第七版(GPT 重译)(六)
67 4