返回介绍

10.7 Vector 类第5版:格式化

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

Vector 类的 __format__ 方法与 Vector2d 类的相似,但是不使用极坐标,而使用球面坐标(也叫超球面坐标),因为 Vector 类支持 n 个维度,而超过四维后,球体变成了“超球体”。8 因此,我们会把自定义的格式后缀由 'p' 变成 'h'。

8Wolfram Mathworld 网站中有一篇介绍超球体的文章;维基百科会把“超球体”词条重定向到“n 维球体”词条。

 9.5 节说过,扩展格式规范微语言时,最好避免重用内置类型支持的格式代码。这里对微语言的扩展还会用到浮点数的格式代码 'eEfFgGn%',而且保持原意,因此绝对要避免重用代码。整数使用的格式代码有 'bcdoxXn',字符串使用的是 's'。在 Vector2d 类中,我选择使用 'p' 表示极坐标。使用 'h' 表示超球面坐标(hyperspherical coordinate)是个不错的选择。

例如,对四维空间(len(v) == 4)中的 Vector 对象来说,'h' 代码得到的结果是这样:<r, Φ1, Φ2, Φ3>。其中,r 是模(abs(v)),余下三个数是角坐标 Φ1、Φ2 和 Φ3。 下面几个示例摘自 vector_v5.py 的 doctest(参见示例 10-16),是四维球面坐标格式:

>>> format(Vector([-1, -1, -1, -1]), 'h')
'<2.0, 2.0943951023931957, 2.186276035465284, 3.9269908169872414>'
>>> format(Vector([2, 2, 2, 2]), '.3eh')
'<4.000e+00, 1.047e+00, 9.553e-01, 7.854e-01>'
>>> format(Vector([0, 1, 0, 0]), '0.5fh')
'<1.00000, 1.57080, 0.00000, 0.00000>'

在小幅改动 __format__ 方法之前,我们要定义两个辅助方法:一个是 angle(n),用于计算某个角坐标(如 Φ1);另一个是 angles(),返回由所有角坐标构成的可迭代对象。我们不会讲解其中涉及的数学原理,如果你好奇的话,可以查看维基百科中的“n 维球体”词条,那里有几个公式,我就是使用它们把 Vector 实例分量数组内的笛卡儿坐标转换成球面坐标的。

示例 10-16 是 vector_v5.py 脚本的完整代码,包含自 10.2 节以来实现的所有代码和本节实现的自定义格式。

示例 10-16  vector_v5.py:Vector 类最终版的 doctest 和全部代码;带标号的那几行是为了支持 __format__ 方法而添加的代码

"""
A multidimensional ``Vector`` class, take 5

A ``Vector`` is built from an iterable of numbers::

  >>> Vector([3.1, 4.2])
  Vector([3.1, 4.2])
  >>> Vector((3, 4, 5))
  Vector([3.0, 4.0, 5.0])
  >>> Vector(range(10))
  Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...])


Tests with two dimensions (same results as ``vector2d_v1.py``)::

  >>> v1 = Vector([3, 4])
  >>> x, y = v1
  >>> x, y
  (3.0, 4.0)
  >>> v1
  Vector([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(Vector([0, 0]))
  (True, False)


Test of ``.frombytes()`` class method:

  >>> v1_clone = Vector.frombytes(bytes(v1))
  >>> v1_clone
  Vector([3.0, 4.0])
  >>> v1 == v1_clone
  True


Tests with three dimensions::

  >>> v1 = Vector([3, 4, 5])
  >>> x, y, z = v1
  >>> x, y, z
  (3.0, 4.0, 5.0)
  >>> v1
  Vector([3.0, 4.0, 5.0])
  >>> v1_clone = eval(repr(v1))
  >>> v1 == v1_clone
  True
  >>> print(v1)
  (3.0, 4.0, 5.0)
  >>> abs(v1)  # doctest:+ELLIPSIS
  7.071067811...
  >>> bool(v1), bool(Vector([0, 0, 0]))
  (True, False)


Tests with many dimensions::

  >>> v7 = Vector(range(7))
  >>> v7
  Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...])
  >>> abs(v7)  # doctest:+ELLIPSIS
  9.53939201...

Test of ``.__bytes__`` and ``.frombytes()`` methods::

  >>> v1 = Vector([3, 4, 5])
  >>> v1_clone = Vector.frombytes(bytes(v1))
  >>> v1_clone
  Vector([3.0, 4.0, 5.0])
  >>> v1 == v1_clone
  True


Tests of sequence behavior::

  >>> v1 = Vector([3, 4, 5])
  >>> len(v1)
  3
  >>> v1[0], v1[len(v1)-1], v1[-1]
  (3.0, 5.0, 5.0)


Test of slicing::

  >>> v7 = Vector(range(7))
  >>> v7[-1]
  6.0
  >>> v7[1:4]
  Vector([1.0, 2.0, 3.0])
  >>> v7[-1:]
  Vector([6.0])
  >>> v7[1,2]
  Traceback (most recent call last):
    ...
  TypeError: Vector indices must be integers


Tests of dynamic attribute access::

  >>> v7 = Vector(range(10))
  >>> v7.x
  0.0
  >>> v7.y, v7.z, v7.t
  (1.0, 2.0, 3.0)

Dynamic attribute lookup failures::

  >>> v7.k
  Traceback (most recent call last):
    ...
  AttributeError: 'Vector' object has no attribute 'k'
  >>> v3 = Vector(range(3))
  >>> v3.t
  Traceback (most recent call last):
    ...
  AttributeError: 'Vector' object has no attribute 't'
  >>> v3.spam
  Traceback (most recent call last):
    ...
  AttributeError: 'Vector' object has no attribute 'spam'


Tests of hashing::

  >>> v1 = Vector([3, 4])
  >>> v2 = Vector([3.1, 4.2])
  >>> v3 = Vector([3, 4, 5])
  >>> v6 = Vector(range(6))
  >>> hash(v1), hash(v3), hash(v6)
  (7, 2, 1)


Most hash values of non-integers vary from a 32-bit to 64-bit CPython build::

  >>> import sys
  >>> hash(v2) == (384307168202284039 if sys.maxsize > 2**32 else 357915986)
  True


Tests of ``format()`` with Cartesian coordinates in 2D::

  >>> v1 = Vector([3, 4])
  >>> format(v1)
  '(3.0, 4.0)'
  >>> format(v1, '.2f')
  '(3.00, 4.00)'
  >>> format(v1, '.3e')
  '(3.000e+00, 4.000e+00)'


Tests of ``format()`` with Cartesian coordinates in 3D and 7D::

  >>> v3 = Vector([3, 4, 5])
  >>> format(v3)
  '(3.0, 4.0, 5.0)'
  >>> format(Vector(range(7)))
  '(0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0)'


Tests of ``format()`` with spherical coordinates in 2D, 3D and 4D::

  >>> format(Vector([1, 1]), 'h')  # doctest:+ELLIPSIS
  '<1.414213..., 0.785398...>'
  >>> format(Vector([1, 1]), '.3eh')
  '<1.414e+00, 7.854e-01>'
  >>> format(Vector([1, 1]), '0.5fh')
  '<1.41421, 0.78540>'
  >>> format(Vector([1, 1, 1]), 'h')  # doctest:+ELLIPSIS
  '<1.73205..., 0.95531..., 0.78539...>'
  >>> format(Vector([2, 2, 2]), '.3eh')
  '<3.464e+00, 9.553e-01, 7.854e-01>'
  >>> format(Vector([0, 0, 0]), '0.5fh')
  '<0.00000, 0.00000, 0.00000>'
  >>> format(Vector([-1, -1, -1, -1]), 'h')  # doctest:+ELLIPSIS
  '<2.0, 2.09439..., 2.18627..., 3.92699...>'
  >>> format(Vector([2, 2, 2, 2]), '.3eh')
  '<4.000e+00, 1.047e+00, 9.553e-01, 7.854e-01>'
  >>> format(Vector([0, 1, 0, 0]), '0.5fh')
  '<1.00000, 1.57080, 0.00000, 0.00000>'
"""

from array import array
import reprlib
import math
import numbers
import functools
import operator
import itertools  ➊


class Vector:
  typecode = 'd'

  def __init__(self, components):
    self._components = array(self.typecode, components)

  def __iter__(self):
    return iter(self._components)

  def __repr__(self):
    components = reprlib.repr(self._components)
    components = components[components.find('['):-1]
    return 'Vector({})'.format(components)

  def __str__(self):
    return str(tuple(self))

  def __bytes__(self):
    return (bytes([ord(self.typecode)]) +
        bytes(self._components))

  def __eq__(self, other):
    return (len(self) == len(other) and
        all(a == b for a, b in zip(self, other)))

  def __hash__(self):
    hashes = (hash(x) for x in self)
    return functools.reduce(operator.xor, hashes, 0)

  def __abs__(self):
    return math.sqrt(sum(x * x for x in self))

  def __bool__(self):
    return bool(abs(self))

  def __len__(self):
    return len(self._components)

  def __getitem__(self, index):
    cls = type(self)
    if isinstance(index, slice):
      return cls(self._components[index])
    elif isinstance(index, numbers.Integral):
      return self._components[index]
    else:
      msg = '{.__name__} indices must be integers'
      raise TypeError(msg.format(cls))

  shortcut_names = 'xyzt'

  def __getattr__(self, name):
    cls = type(self)
    if len(name) == 1:
      pos = cls.shortcut_names.find(name)
      if 0 <= pos < len(self._components):
        return self._components[pos]
    msg = '{.__name__!r} object has no attribute {!r}'
    raise AttributeError(msg.format(cls, name))

  def angle(self, n):  ➋
    r = math.sqrt(sum(x * x for x in self[n:]))
    a = math.atan2(r, self[n-1])
    if (n == len(self) - 1) and (self[-1] < 0):
      return math.pi * 2 - a
    else:
      return a

  def angles(self):  ➌
    return (self.angle(n) for n in range(1, len(self)))

  def __format__(self, fmt_spec=''):
    if fmt_spec.endswith('h'):  # 超球面坐标
      fmt_spec = fmt_spec[:-1]
      coords = itertools.chain([abs(self)],
                   self.angles())  ➍
      outer_fmt = '<{}>'  ➎
    else:
      coords = self
      outer_fmt = '({})'  ➏
    components = (format(c, fmt_spec) for c in coords)  ➐
    return outer_fmt.format(', '.join(components))  ➑

  @classmethod
  def frombytes(cls, octets):
    typecode = chr(octets[0])
    memv = memoryview(octets[1:]).cast(typecode)
    return cls(memv)

❶ 为了在 __format__ 方法中使用 chain 函数,导入 itertools 模块。

❷ 使用“n 维球体”词条中的公式计算某个角坐标。

❸ 创建生成器表达式,按需计算所有角坐标。

❹ 使用 itertools.chain 函数生成生成器表达式,无缝迭代向量的模和各个角坐标。

❺ 配置使用尖括号显示球面坐标。

❻ 配置使用圆括号显示笛卡儿坐标。

❼ 创建生成器表达式,按需格式化各个坐标元素。

❽ 把以逗号分隔的格式化分量插入尖括号或圆括号。

 我们在 __format__、angle 和 angles 中大量使用了生成器表达式,不过我们的目的是让 Vector 类的 __format__ 方法与 Vector2d 类处在同一水平上。第 14 章讨论生成器时会使用 Vector 类中的部分代码举例,然后详细说明生成器的技巧。

本章的任务到此结束。第 13 章会改进 Vector 类,让它支持中缀运算符。本章的目的是探讨如何编写集合类广泛使用的几个特殊方法。

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

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

发布评论

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