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

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

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

要使Vector2d与位置模式配合使用,我们需要添加一个名为__match_args__的类属性,按照它们将用于位置模式匹配的顺序列出实例属性:

class Vector2d:
    __match_args__ = ('x', 'y')
    # etc...

现在,当编写用于匹配Vector2d主题的模式时,我们可以节省一些按键,如您在示例 11-10 中所见。

示例 11-10。Vector2d主题的位置模式——需要 Python 3.10
def positional_pattern_demo(v: Vector2d) -> None:
    match v:
        case Vector2d(0, 0):
            print(f'{v!r} is null')
        case Vector2d(0):
            print(f'{v!r} is vertical')
        case Vector2d(_, 0):
            print(f'{v!r} is horizontal')
        case Vector2d(x, y) if x==y:
            print(f'{v!r} is diagonal')
        case _:
            print(f'{v!r} is awesome')

__match_args__类属性不需要包括所有公共实例属性。特别是,如果类__init__具有分配给实例属性的必需和可选参数,可能合理地在__match_args__中命名必需参数,但不包括可选参数。

让我们退后一步,回顾一下我们到目前为止在Vector2d中编码的内容。

Vector2d 的完整列表,版本 3

我们已经在Vector2d上工作了一段时间,只展示了一些片段,因此示例 11-11 是vector2d_v3.py的综合完整列表,包括我在开发时使用的 doctests。

示例 11-11。vector2d_v3.py:完整的版本
"""
A two-dimensional vector class
 >>> v1 = Vector2d(3, 4)
 >>> print(v1.x, v1.y)
 3.0 4.0
 >>> x, y = v1
 >>> x, y
 (3.0, 4.0)
 >>> v1
 Vector2d(3.0, 4.0)
 >>> v1_clone = eval(repr(v1))
 >>> v1 == v1_clone
 True
 >>> print(v1)
 (3.0, 4.0)
 >>> octets = bytes(v1)
 >>> octets
 b'd\\x00\\x00\\x00\\x00\\x00\\x00\\x08@\\x00\\x00\\x00\\x00\\x00\\x00\\x10@'
 >>> abs(v1)
 5.0
 >>> bool(v1), bool(Vector2d(0, 0))
 (True, False)
Test of ``.frombytes()`` class method:
 >>> v1_clone = Vector2d.frombytes(bytes(v1))
 >>> v1_clone
 Vector2d(3.0, 4.0)
 >>> v1 == v1_clone
 True
Tests of ``format()`` with Cartesian coordinates:
 >>> format(v1)
 '(3.0, 4.0)'
 >>> format(v1, '.2f')
 '(3.00, 4.00)'
 >>> format(v1, '.3e')
 '(3.000e+00, 4.000e+00)'
Tests of the ``angle`` method::
 >>> Vector2d(0, 0).angle()
 0.0
 >>> Vector2d(1, 0).angle()
 0.0
 >>> epsilon = 10**-8
 >>> abs(Vector2d(0, 1).angle() - math.pi/2) < epsilon
 True
 >>> abs(Vector2d(1, 1).angle() - math.pi/4) < epsilon
 True
Tests of ``format()`` with polar coordinates:
 >>> format(Vector2d(1, 1), 'p')  # doctest:+ELLIPSIS
 '<1.414213..., 0.785398...>'
 >>> format(Vector2d(1, 1), '.3ep')
 '<1.414e+00, 7.854e-01>'
 >>> format(Vector2d(1, 1), '0.5fp')
 '<1.41421, 0.78540>'
Tests of `x` and `y` read-only properties:
 >>> v1.x, v1.y
 (3.0, 4.0)
 >>> v1.x = 123
 Traceback (most recent call last):
 ...
 AttributeError: can't set attribute 'x'
Tests of hashing:
 >>> v1 = Vector2d(3, 4)
 >>> v2 = Vector2d(3.1, 4.2)
 >>> len({v1, v2})
 2
"""
from array import array
import math
class Vector2d:
    __match_args__ = ('x', 'y')
    typecode = 'd'
    def __init__(self, x, y):
        self.__x = float(x)
        self.__y = float(y)
    @property
    def x(self):
        return self.__x
    @property
    def y(self):
        return self.__y
    def __iter__(self):
        return (i for i in (self.x, self.y))
    def __repr__(self):
        class_name = type(self).__name__
        return '{}({!r}, {!r})'.format(class_name, *self)
    def __str__(self):
        return str(tuple(self))
    def __bytes__(self):
        return (bytes([ord(self.typecode)]) +
                bytes(array(self.typecode, self)))
    def __eq__(self, other):
        return tuple(self) == tuple(other)
    def __hash__(self):
        return hash((self.x, self.y))
    def __abs__(self):
        return math.hypot(self.x, self.y)
    def __bool__(self):
        return bool(abs(self))
    def angle(self):
        return math.atan2(self.y, self.x)
    def __format__(self, fmt_spec=''):
        if fmt_spec.endswith('p'):
            fmt_spec = fmt_spec[:-1]
            coords = (abs(self), self.angle())
            outer_fmt = '<{}, {}>'
        else:
            coords = self
            outer_fmt = '({}, {})'
        components = (format(c, fmt_spec) for c in coords)
        return outer_fmt.format(*components)
    @classmethod
    def frombytes(cls, octets):
        typecode = chr(octets[0])
        memv = memoryview(octets[1:]).cast(typecode)
        return cls(*memv)

总结一下,在本节和前几节中,我们看到了一些您可能希望实现以拥有完整对象的基本特殊方法。

注意

只有在您的应用程序需要时才实现这些特殊方法。最终用户不在乎构成应用程序的对象是否“Pythonic”。

另一方面,如果您的类是其他 Python 程序员使用的库的一部分,您实际上无法猜测他们将如何处理您的对象,他们可能期望我们正在描述的更多“Pythonic”行为。

如示例 11-11 中所编码的,Vector2d是一个关于对象表示相关特殊方法的教学示例,而不是每个用户定义类的模板。

在下一节中,我们将暂时离开Vector2d,讨论 Python 中私有属性机制的设计和缺点——self.__x中的双下划线前缀。

Python 中的私有和“受保护”的属性

在 Python 中,没有像 Java 中的private修饰符那样创建私有变量的方法。在 Python 中,我们有一个简单的机制来防止在子类中意外覆盖“私有”属性。

考虑这种情况:有人编写了一个名为Dog的类,其中内部使用了一个mood实例属性,但没有暴露它。你需要将Dog作为Beagle的子类。如果你在不知道名称冲突的情况下创建自己的mood实例属性,那么你将覆盖从Dog继承的方法中使用的mood属性。这将是一个令人头疼的调试问题。

为了防止这种情况发生,如果你将一个实例属性命名为__mood(两个前导下划线和零个或最多一个尾随下划线),Python 会将该名称存储在实例__dict__中,前缀是一个前导下划线和类名,因此在Dog类中,__mood变成了_Dog__mood,而在Beagle中变成了_Beagle__mood。这种语言特性被称为名称修饰

示例 11-12 展示了来自示例 11-7 中Vector2d类的结果。

示例 11-12. 私有属性名称通过前缀_和类名“修饰”
>>> v1 = Vector2d(3, 4)
>>> v1.__dict__
{'_Vector2d__y': 4.0, '_Vector2d__x': 3.0}
>>> v1._Vector2d__x
3.0

名称修饰是关于安全性,而不是安全性:它旨在防止意外访问,而不是恶意窥探。图 11-1 展示了另一个安全设备。

知道私有名称是如何被修饰的人可以直接读取私有属性,就像示例 11-12 的最后一行所示的那样——这对调试和序列化实际上是有用的。他们还可以通过编写v1._Vector2d__x = 7来直接为Vector2d的私有组件赋值。但如果你在生产代码中这样做,如果出现问题,就不能抱怨了。

名称修饰功能并不受所有 Python 爱好者的喜爱,以及写作为self.__x的名称的倾斜外观也不受欢迎。一些人更喜欢避免这种语法,只使用一个下划线前缀通过约定“保护”属性(例如,self._x)。对于自动双下划线修饰的批评者,他们建议通过命名约定来解决意外属性覆盖的问题。Ian Bicking——pip、virtualenv 等项目的创建者写道:

永远不要使用两个前导下划线。这是非常私有的。如果担心名称冲突,可以使用显式的名称修饰(例如,_MyThing_blahblah)。这与双下划线基本相同,只是双下划线会隐藏,而显式名称修饰则是透明的。⁷

图 11-1. 开关上的盖子是一个安全设备,而不是安全设备:它防止事故,而不是破坏。

单个下划线前缀在属性名称中对 Python 解释器没有特殊含义,但在 Python 程序员中是一个非常强烈的约定,你不应该从类外部访问这样的属性。⁸。尊重一个将其属性标记为单个下划线的对象的隐私是很容易的,就像尊重将ALL_CAPS中的变量视为常量的约定一样容易。

在 Python 文档的某些角落中,带有单个下划线前缀的属性被称为“受保护的”⁹。通过约定以self._x的形式“保护”属性的做法很普遍,但将其称为“受保护的”属性并不那么常见。有些人甚至将其称为“私有”属性。

总之:Vector2d的组件是“私有的”,我们的Vector2d实例是“不可变的”——带有引号——因为没有办法使它们真正私有和不可变。¹⁰

现在我们回到我们的Vector2d类。在下一节中,我们将介绍一个特殊的属性(不是方法),它会影响对象的内部存储,对内存使用可能有巨大影响,但对其公共接口影响很小:__slots__

使用__slots__节省内存

默认情况下,Python 将每个实例的属性存储在名为__dict__dict中。正如我们在“dict 工作原理的实际后果”中看到的,dict具有显着的内存开销——即使使用了该部分提到的优化。但是,如果你定义一个名为__slots__的类属性,其中包含一系列属性名称,Python 将使用替代的存储模型来存储实例属性:__slots__中命名的属性存储在一个隐藏的引用数组中,使用的内存比dict少。让我们通过简单的示例来看看它是如何工作的,从示例 11-13 开始。

示例 11-13。Pixel类使用__slots__
>>> class Pixel:
...     __slots__ = ('x', 'y')  # ①
...
>>> p = Pixel()  # ②
>>> p.__dict__  # ③
Traceback (most recent call last):
  ...
AttributeError: 'Pixel' object has no attribute '__dict__'
>>> p.x = 10  # ④
>>> p.y = 20
>>> p.color = 'red'  # ⑤
Traceback (most recent call last):
  ...
AttributeError: 'Pixel' object has no attribute 'color'

在创建类时必须存在__slots__;稍后添加或更改它没有效果。属性名称可以是tuplelist,但我更喜欢tuple,以明确表明没有改变的必要。

创建一个Pixel的实例,因为我们看到__slots__对实例的影响。

第一个效果:Pixel的实例没有__dict__

正常设置p.xp.y属性。

第二个效果:尝试设置一个未在__slots__中列出的属性会引发AttributeError

到目前为止,一切顺利。现在让我们在示例 11-14 中创建Pixel的一个子类,看看__slots__的反直觉之处。

示例 11-14。OpenPixelPixel的子类
>>> class OpenPixel(Pixel):  # ①
...     pass
...
>>> op = OpenPixel()
>>> op.__dict__  # ②
{} >>> op.x = 8  # ③
>>> op.__dict__  # ④
{} >>> op.x  # ⑤
8 >>> op.color = 'green'  # ⑥
>>> op.__dict__  # ⑦
{'color': 'green'}

OpenPixel没有声明自己的属性。

惊喜:OpenPixel的实例有一个__dict__

如果你设置属性x(在基类Pixel__slots__中命名)…

…它不存储在实例__dict__中…

…但它存储在实例的隐藏引用数组中。

如果你设置一个未在__slots__中命名的属性…

…它存储在实例__dict__中。

示例 11-14 显示了__slots__的效果只被子类部分继承。为了确保子类的实例没有__dict__,你必须在子类中再次声明__slots__

如果你声明__slots__ = ()(一个空元组),那么子类的实例将没有__dict__,并且只接受基类__slots__中命名的属性。

如果你希望子类具有额外的属性,请在__slots__中命名它们,就像示例 11-15 中所示的那样。

示例 11-15。ColorPixelPixel的另一个子类
>>> class ColorPixel(Pixel):
...    __slots__ = ('color',)  # ①
>>> cp = ColorPixel()
>>> cp.__dict__  # ②
Traceback (most recent call last):
  ...
AttributeError: 'ColorPixel' object has no attribute '__dict__'
>>> cp.x = 2
>>> cp.color = 'blue'  # ③
>>> cp.flavor = 'banana'
Traceback (most recent call last):
  ...
AttributeError: 'ColorPixel' object has no attribute 'flavor'

本质上,超类的__slots__被添加到当前类的__slots__中。不要忘记单项元组必须有一个尾随逗号。

ColorPixel实例没有__dict__

你可以设置此类和超类的__slots__中声明的属性,但不能设置其他属性。

“既能节省内存又能使用它”是可能的:如果将'__dict__'名称添加到__slots__列表中,那么你的实例将保留__slots__中命名的属性在每个实例的引用数组中,但也将支持动态创建的属性,这些属性将存储在通常的__dict__中。如果你想要使用@cached_property装饰器(在“第 5 步:使用 functools 缓存属性”中介绍),这是必要的。

当然,在__slots__中有'__dict__'可能完全打败它的目的,这取决于每个实例中静态和动态属性的数量以及它们的使用方式。粗心的优化比过早的优化更糟糕:你增加了复杂性,但可能得不到任何好处。

另一个你可能想要保留的特殊每实例属性是__weakref__,这对于对象支持弱引用是必要的(在“del 和垃圾回收”中简要提到)。该属性默认存在于用户定义类的实例中。但是,如果类定义了__slots__,并且你需要实例成为弱引用的目标,则需要在__slots__中包含'__weakref__'

现在让我们看看将__slots__添加到Vector2d的效果。

简单的节省度量

示例 11-16 展示了在Vector2d中实现__slots__

示例 11-16. vector2d_v3_slots.py:__slots__属性是Vector2d的唯一添加
class Vector2d:
    __match_args__ = ('x', 'y')  # ①
    __slots__ = ('__x', '__y')  # ②
    typecode = 'd'
    # methods are the same as previous version

__match_args__列出了用于位置模式匹配的公共属性名称。

相比之下,__slots__列出了实例属性的名称,这些属性在这种情况下是私有属性。

为了测量内存节省,我编写了mem_test.py脚本。它接受一个带有Vector2d类变体的模块名称作为命令行参数,并使用列表推导式构建一个包含 10,000,000 个Vector2d实例的list。在示例 11-17 中显示的第一次运行中,我使用vector2d_v3.Vector2d(来自示例 11-7);在第二次运行中,我使用具有__slots__的版本,来自示例 11-16。

示例 11-17. mem_test.py 创建了 10 百万个Vector2d实例,使用了命名模块中定义的类
$ time python3 mem_test.py vector2d_v3
Selected Vector2d type: vector2d_v3.Vector2d
Creating 10,000,000 Vector2d instances
Initial RAM usage:      6,983,680
  Final RAM usage:  1,666,535,424
real  0m11.990s
user  0m10.861s
sys 0m0.978s
$ time python3 mem_test.py vector2d_v3_slots
Selected Vector2d type: vector2d_v3_slots.Vector2d
Creating 10,000,000 Vector2d instances
Initial RAM usage:      6,995,968
  Final RAM usage:    577,839,104
real  0m8.381s
user  0m8.006s
sys 0m0.352s

如示例 11-17 所示,当每个 10 百万个Vector2d实例中使用__dict__时,脚本的 RAM 占用量增长到了 1.55 GiB,但当Vector2d具有__slots__属性时,降低到了 551 MiB。__slots__版本也更快。这个测试中的mem_test.py脚本基本上处理加载模块、检查内存使用情况和格式化结果。你可以在fluentpython/example-code-2e存储库中找到它的源代码。

提示

如果你处理数百万个具有数值数据的对象,你应该真的使用 NumPy 数组(参见“NumPy”),它们不仅内存高效,而且具有高度优化的数值处理函数,其中许多函数一次操作整个数组。我设计Vector2d类只是为了在讨论特殊方法时提供背景,因为我尽量避免在可以的情况下使用模糊的foobar示例。

总结__slots__的问题

如果正确使用,__slots__类属性可能会提供显著的内存节省,但有一些注意事项:

  • 你必须记得在每个子类中重新声明__slots__,以防止它们的实例具有__dict__
  • 实例只能拥有__slots__中列出的属性,除非在__slots__中包含'__dict__'(但这样做可能会抵消内存节省)。
  • 使用__slots__的类不能使用@cached_property装饰器,除非在__slots__中明确命名'__dict__'
  • 实例不能成为弱引用的目标,除非在__slots__中添加'__weakref__'

本章的最后一个主题涉及在实例和子类中覆盖类属性。

覆盖类属性

Python 的一个显著特点是类属性可以用作实例属性的默认值。在Vector2d中有typecode类属性。它在__bytes__方法中使用了两次,但我们设计上将其读取为self.typecode。因为Vector2d实例是在没有自己的typecode属性的情况下创建的,所以self.typecode将默认获取Vector2d.typecode类属性。

但是,如果写入一个不存在的实例属性,就会创建一个新的实例属性,例如,一个typecode实例属性,而同名的类属性则保持不变。但是,从那时起,每当处理该实例的代码读取self.typecode时,实例typecode将被检索,有效地遮蔽了同名的类属性。这打开了使用不同typecode自定义单个实例的可能性。

默认的Vector2d.typecode'd',意味着每个向量分量在导出为bytes时将被表示为 8 字节的双精度浮点数。如果在导出之前将Vector2d实例的typecode设置为'f',则每个分量将以 4 字节的单精度浮点数导出。示例 11-18 演示了这一点。

注意

我们正在讨论添加自定义实例属性,因此示例 11-18 使用了没有__slots__Vector2d实现,如示例 11-11 中所列。

示例 11-18。通过设置以前从类继承的typecode属性来自定义实例
>>> from vector2d_v3 import Vector2d
>>> v1 = Vector2d(1.1, 2.2)
>>> dumpd = bytes(v1)
>>> dumpd
b'd\x9a\x99\x99\x99\x99\x99\xf1?\x9a\x99\x99\x99\x99\x99\x01@' >>> len(dumpd)  # ①
17 >>> v1.typecode = 'f'  # ②
>>> dumpf = bytes(v1)
>>> dumpf
b'f\xcd\xcc\x8c?\xcd\xcc\x0c@' >>> len(dumpf)  # ③
9 >>> Vector2d.typecode  # ④
'd'

①](#co_a_pythonic_object_CO13-1)

默认的bytes表示长度为 17 字节。

v1实例中将typecode设置为'f'

现在bytes转储的长度为 9 字节。

Vector2d.typecode保持不变;只有v1实例使用typecode'f'

现在应该清楚为什么Vector2dbytes导出以typecode为前缀:我们想要支持不同的导出格式。

如果要更改类属性,必须直接在类上设置,而不是通过实例。你可以通过以下方式更改所有实例(没有自己的typecode)的默认typecode

>>> Vector2d.typecode = 'f'

然而,在 Python 中有一种惯用的方法可以实现更持久的效果,并且更明确地说明更改。因为类属性是公共的,它们会被子类继承,所以习惯上是通过子类来定制类数据属性。Django 类基视图广泛使用这种技术。示例 11-19 展示了如何实现。

示例 11-19。ShortVector2dVector2d的子类,只覆盖了默认的typecode
>>> from vector2d_v3 import Vector2d
>>> class ShortVector2d(Vector2d):  # ①
...     typecode = 'f'
...
>>> sv = ShortVector2d(1/11, 1/27)  # ②
>>> sv
ShortVector2d(0.09090909090909091, 0.037037037037037035) # ③
>>> len(bytes(sv))  # ④
9

创建ShortVector2d作为Vector2d的子类,只是为了覆盖typecode类属性。

为演示构建ShortVector2d实例sv

检查svrepr

检查导出字节的长度为 9,而不是之前的 17。

这个例子还解释了为什么我没有在Vector2d.__repr__中硬编码class_name,而是从type(self).__name__获取它,就像这样:

# inside class Vector2d:
    def __repr__(self):
        class_name = type(self).__name__
        return '{}({!r}, {!r})'.format(class_name, *self)

如果我在class_name中硬编码,Vector2d的子类如ShortVector2d将不得不覆盖__repr__以更改class_name。通过从实例的type中读取名称,我使__repr__更安全地继承。

我们结束了构建一个简单类的覆盖,利用数据模型与 Python 的其他部分协作:提供不同的对象表示,提供自定义格式代码,公开只读属性,并支持 hash() 以与集合和映射集成。


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

相关文章
|
7天前
|
数据采集 存储 人工智能
【Python+微信】【企业微信开发入坑指北】4. 企业微信接入GPT,只需一个URL,自动获取文章总结
【Python+微信】【企业微信开发入坑指北】4. 企业微信接入GPT,只需一个URL,自动获取文章总结
21 0
|
12天前
|
机器学习/深度学习 人工智能 自然语言处理
总结几个GPT的超实用之处【附带Python案例】
总结几个GPT的超实用之处【附带Python案例】
|
前端开发 JavaScript 算法
JavaScript 权威指南第七版(GPT 重译)(七)(3)
JavaScript 权威指南第七版(GPT 重译)(七)
32 0
|
前端开发 JavaScript 算法
JavaScript 权威指南第七版(GPT 重译)(七)(1)
JavaScript 权威指南第七版(GPT 重译)(七)
60 0
|
12天前
|
存储 前端开发 JavaScript
JavaScript 权威指南第七版(GPT 重译)(六)(4)
JavaScript 权威指南第七版(GPT 重译)(六)
90 2
JavaScript 权威指南第七版(GPT 重译)(六)(4)
|
12天前
|
前端开发 JavaScript API
JavaScript 权威指南第七版(GPT 重译)(六)(3)
JavaScript 权威指南第七版(GPT 重译)(六)
55 4
|
12天前
|
JSON 前端开发 JavaScript
JavaScript 权威指南第七版(GPT 重译)(五)(2)
JavaScript 权威指南第七版(GPT 重译)(五)
35 5
|
12天前
|
JSON JavaScript 前端开发
JavaScript 权威指南第七版(GPT 重译)(四)(4)
JavaScript 权威指南第七版(GPT 重译)(四)
67 6
|
12天前
|
Web App开发 前端开发 JavaScript
JavaScript 权威指南第七版(GPT 重译)(四)(1)
JavaScript 权威指南第七版(GPT 重译)(四)
35 2
|
12天前
|
存储 JavaScript 前端开发
JavaScript 权威指南第七版(GPT 重译)(三)(3)
JavaScript 权威指南第七版(GPT 重译)(三)
41 1