得益于 Python 数据模型,自定义类型的行为可以像内置类型那样自然。 实现如此自然的行为,靠的不是继承,而是鸭子类型(duck typing): 我们只需按照预定行为实现对象所需的方法即可。
前一章分析了很多内置对象的结构和行为,这一章则自己定义类,而且让类的行为跟真正的 Python 对象一样。
本章包含以下话题:
- 支持用于生成对象其他表示形式的内置函数( 如 repr()、bytes() 等等 ) ;
- 使用一个类方法实现备选构造方法;
- 扩展内置的 format() 函数和 str.format() 方法使用的格式微语言;
- 实现只读属性;
- 把对象变为可散列的,以便在集合中及作为 dict 的键使用;
- 利用 __slots__ 节省内存;
我们将开发一个简单的二维欧几里得向量类型,在这个过程中涵盖上述全部话题。
在实现这个类型的中间阶段,我们会讨论两个概念:
- 如何以及何时使用 @classmethod 和 @staticmethod 装饰器
- Python 的私有属性和受保护属性的用法、约定和局限
我们从对象表示形式函数开始。
9.1 对象表示形式
每门面向对象的语言至少都有一种获取对象的字符串表示形式的标准方式。
Python 提供了两种方式:
-
repr():以便于开发者理解的方式返回对象的字符串表示形式。
-
str():以便于用户理解的方式返回对象的字符串表示形式。
正如你所知,我们要实现 __repr__ 和 __str__ 特殊方法,为 repr() 和 str() 提供支持。
为了给对象提供其他的表示形式,还会用到另外两个特殊方 法:__bytes__ 和 __format__。__bytes__ 方法与 __str__ 方法类似:bytes() 函数调用它获取对象的字节序列表示形式。而 __format__ 方法会被内置的 format() 函数和 str.format() 方法调用,使用特殊的格式代码显示对象的字符串表示形式。我们将在下一个示例中讨论 __bytes__ 方法,随后再讨论 __format__ 方法。
9.2 再谈向量类
为了说明用于生成对象表示形式的众多方法,我们将使用一个 Vector2d 类,它与第 1 章中的类似。
我们期望 Vector2d 实例具有的基本行为如 示例 9-1 所示:
v1 = Vector2d(3, 4)
# 1. Vector2d 实例的分量可以直接通过属性访问(无需调用读值方法)。
print(v1.x, v1.y) # 3.0 4.0
# 2. Vector2d 实例可以拆包成变量元组。
x, y = v1
print(x, y) # 3.0 4.0
# 3. repr 函数调用 Vector2d 实例,得到的结果类似于构建实例的源码。
print(repr(v1)) # Vector2d(3.0, 4.0)
# 4. 这里使用 eval 函数,表明 repr 函数调用 Vector2d 实例得到的是对构造方法的准确表述。
v1_clone = eval(repr(v1))
# Vector2d 实例支持使用 == 比较,这样便于测试。
print(v1 == v1_clone) # True
# print 函数会调用 str 函数,对 Vector2d 来说,输出的是一个有序对。
print(v1) # (3.0, 4.0)
# 7. bytes 函数会调用 __bytes__ 方法,生成实例的二进制表示形式。
octets = bytes(v1)
print(octets) # b'd\x00\x00\x00\x00\x00\x00\x08@\x00\x00\x00\x00\x00\x00\x10@'
# 8. abs 函数会调用 __abs__ 方法,返回 Vector2d 实例的模。
print(abs(v1)) # 5.0
# 9. bool 函数会调用 __bool__ 方法,如果 Vector2d 实例的模为零,返回 False,否则返回 True。
print(bool(v1)) # True
print(bool(Vector2d(0, 0))) # False
示例 9-2 Vector2d 类实现支持上述行为的特殊方法:
import math
from array import array
class Vector2d:
# typeCode 是类属性,在 Vector2d 实例和字节序列之间转换时使用。
typeCode = 'd'
# 在 __init__ 方法中把 x 和 y 转换成浮点数,尽早捕获错误,以防调用 Vector2d 函数时传入不当参数。
def __init__(self, x, y):
self.x = float(x)
self.y = float(y)
# 定义 __iter__ 方法,把 Vector2d 实例变成可迭代的对象,这样才能拆包(例如,x, y = my_vector)。
def __iter__(self):
return (i for i in (self.x, self.y))
# 使用 {!r} 获取各个分量的表示形式,然后插值,构成一个字符串;
# 因为 Vector2d 实例是可迭代的对象,所以 *self 会把 x 和 y 分量提供给 format 函数。
def __repr__(self):
class_name = type(self).__name__
return '{}({!r}, {!r})'.format(class_name, *self)
# 从可迭代的 Vector2d 实例中可以轻松地得到一个元组,显示为一个有序对。
def __str__(self):
return str(tuple(self))
# 为了生成字节序列,把 typeCode 转换成字节序列 + 根据 Vector2d 实例得到一个字节类型数组,再把数组转换成字节序列。
def __bytes__(self):
return bytes([ord(self.typeCode)]) + bytes(array(self.typeCode, self))
# 为了快速比较所有分量,在操作数中构建元组。不过会有问题,参见下面的警告:
# 此方法在两个操作数都是 Vector2d 实例时可用,不过拿 Vector2d 实例与其他具有相同数值的可迭代对象相比,结果也是 True(如 Vector(3, 4) == [3, 4])。
# 这个行为可以视作特性,也可以视作缺陷。
def __eq__(self, other):
return tuple(self) == tuple(other)
# 模是 x 和 y 分量构成的直角三角形的斜边长。
def __abs__(self):
return math.hypot(self.x, self.y)
# __bool__ 方法使用 abs(self) 计算模,然后把结果转换成布尔值,因此,0.0 是 False,非零值是 True。
def __bool__(self):
return bool(abs(self))
⚠️ 注意:示例 9-2 中的 __eq__ 方法,在两个操作数都是 Vector2d 实例时可用,不过拿 Vector2d 实例与其他具有相同数值的可迭代对象相比,结果也是 True(如 Vector(3, 4) == [3, 4] )。这个行为可以视作特性,也可以视作缺陷。
9.3 备选构造方法
我们已经定义了很多基本方法,但是显然少了一个操作:使用 bytes() 函数生成的二进制表示形式重建 Vector2d 实例。
我们可以通过 __bytes__ 把 Vector2d 实例转成字节序列了;同理,也应该能从字节序列转成 Vector2d 实例。在标准库中一番探索后,我们发现 array.array 有个类方法 .frombytes 正好符合我们的需求。下面我们为 Vector2d 定一个同名类方法:
import math
from array import array
class Vector2d:
typeCode = 'd'
def __init__(self, x, y):
self.x = float(x)
self.y = float(y)
def __bytes__(self):
return bytes([ord(self.typeCode)]) + bytes(array(self.typeCode, self))
# 隐藏了大部分魔术方法
......
# 1. 类方法使用 classmethod 装饰器修饰。
@classmethod
# 2. 通过 cls 传入类本身。
def frombytes(cls, octets):
# 3. 获取 typeCode
typecode = chr(octets[0])
# 4. 使用传入的 octets 字节序列创建一个 memoryview,然后使用 typecode 转换。
memv = memoryview(octets[1:]).cast(typecode)
# 5. 拆包转换后的 memoryview,得到构造方法所需的一对参数。
return cls(*memv)