返回介绍

9.6 可散列的 Vector2d

发布于 2024-02-05 21:59:47 字数 6469 浏览 0 评论 0 收藏 0

按照定义,目前 Vector2d 实例是不可散列的,因此不能放入集合(set)中:

>>> v1 = Vector2d(3, 4)
>>> hash(v1)
Traceback (most recent call last):
  ...
TypeError: unhashable type: 'Vector2d'
>>> set([v1])
Traceback (most recent call last):
  ...
TypeError: unhashable type: 'Vector2d'

为了把 Vector2d 实例变成可散列的,必须使用 __hash__ 方法(还需要 __eq__ 方法,前面已经实现了)。此外,还要让向量不可变,详情参见第 3 章的附注栏“什么是可散列的数据类型”。

目前,我们可以为分量赋新值,如 v1.x = 7,Vector2d 类的代码并不阻止这么做。我们想要的行为是这样的:

>>> v1.x, v1.y
(3.0, 4.0)
>>> v1.x = 7
Traceback (most recent call last):
  ...
AttributeError: can't set attribute

为此,我们要把 x 和 y 分量设为只读特性,如示例 9-7 所示。

示例 9-7 vector2d_v3.py:这里只给出了让 Vector2d 不可变的代码,完整的代码清单在示例 9-9 中

class Vector2d:
  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))  ➏

  # 下面是其他方法(排版需要,省略了)

❶ 使用两个前导下划线(尾部没有下划线,或者有一个下划线),把属性标记为私有的。6

6根据本章开头引用的那句话,这不符合 Ian Bicking 的建议。私有属性的优缺点参见后面的 9.7 节。

❷ @property 装饰器把读值方法标记为特性。

❸ 读值方法与公开属性同名,都是 x。

❹ 直接返回 self.__x。

❺ 以同样的方式处理 y 特性。

❻ 需要读取 x 和 y 分量的方法可以保持不变,通过 self.x 和 self.y 读取公开特性,而不必读取私有属性,因此上述代码清单省略了这个类的其他代码。

 Vector.x 和 Vector.y 是只读特性。读写特性在第 19 章讨论,届时会深入说明 @property 装饰器。

注意,我们让这些向量不可变是有原因的,因为这样才能实现 __hash__ 方法。这个方法应该返回一个整数,理想情况下还要考虑对象属性的散列值(__eq__ 方法也要使用),因为相等的对象应该具有相同的散列值。根据特殊方法 __hash__ 的文档,最好使用位运算符异或(^)混合各分量的散列值——我们会这么做。Vector2d.__hash__ 方法的代码十分简单,如示例 9-8 所示。

示例 9-8 vector2d_v3.py:实现 __hash__ 方法

# 在Vector2d类中定义

def __hash__(self):
  return hash(self.x) ^ hash(self.y)

添加 __hash__ 方法之后,向量变成可散列的了:

>>> v1 = Vector2d(3, 4)
>>> v2 = Vector2d(3.1, 4.2)
>>> hash(v1), hash(v2)
(7, 384307168202284039)
>>> set([v1, v2])
{Vector2d(3.1, 4.2), Vector2d(3.0, 4.0)}

 要想创建可散列的类型,不一定要实现特性,也不一定要保护实例属性。只需正确地实现 __hash__ 和 __eq__ 方法即可。但是,实例的散列值绝不应该变化,因此我们借机提到了只读特性。

如果定义的类型有标量数值,可能还要实现 __int__ 和 __float__ 方法(分别被 int() 和 float() 构造函数调用),以便在某些情况下用于强制转换类型。此外,还有用于支持内置的 complex() 构造函数的 __complex__ 方法。Vector2d 或许应该提供 __complex__ 方法,不过我把它留作练习给读者。

我们一直在定义 Vector2d 类,也列出了很多代码片段,示例 9-9 是整理后的完整代码清单,保存在 vector2d_v3.py 文件中,包含开发时我编写的全部 doctest。

示例 9-9 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


Tests of hashing:

  >>> v1 = Vector2d(3, 4)
  >>> v2 = Vector2d(3.1, 4.2)
  >>> hash(v1), hash(v2)
  (7, 384307168202284039)
  >>> len(set([v1, v2]))
  2

"""

from array import array
import math

class Vector2d:
  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) ^ hash(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)

小结一下,前两节说明了一些特殊方法,要想得到功能完善的对象,这些方法可能是必备的。当然,如果你的应用用不到,就没必要全部实现这些方法。客户并不关心你的对象是否符合 Python 风格。

示例 9-9 中定义的 Vector2d 类只是为了教学,我们为它定义了许多与对象表示形式有关的特殊方法。不是每个用户自定义的类都要这样做。

下一节暂时不继续定义 Vector2d 类了,我们将讨论 Python 对私有属性(带两个下划线前缀的属性,如 self.__x)的设计方式及其缺点。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文