对于一个库或框架来说,让 Python 程序员尽可能轻松自然地掌握如何执行任务就是Pythonic
--Martijn Faassen, creator of Python and JavaScript frameworks
得益于 Python 数据模型,用户定义类型可以像内置类型一样自然地运行。这可以在没有继承的情况下完成,靠的鸭子类型:您只需实现对象按预期运行所需的方法。
在前面的章节中,我们研究了许多内置对象的行为。我们现在将构建用户定义的类,使它们的行为就像真正的 Python 对象。您的应用程序类可能不需要也不应该实现本章示例中那么多的特殊方法。但是,如果您正在编写库或框架,那么使用类的程序员可能希望它们的行为类似于 Python 提供的类。实现这种期望是“Pythonic”的一种方式。
本章续接第 1 章,展示了如何实现在许多不同类型的 Python 对象中常见的几种特殊方法。
在本章中,我们将看到如何:
- 支持将对象转换为其他类型的内置函数(例如,repr()、bytes()、complex() 等)。
- 用类方法实现替代构造函数。
- 扩展 f-strings 使用的格式微语言、内置的 format() 和 str.format() 方法。
- 提供对属性的只读访问权限。
- 使对象可散列以用于集合和字典的键。
- 使用 __slots__ 节省内存。
我们将在开发一个简单的二维欧几里得矢量类型 Vector2d 时完成所有这些工作。这段代码将是第 12 章中 N 维向量类的基础。
示例的演示的中间将介绍两个概念性主题:
- 如何以及何时使用 @classmethod 和 @staticmethod 装饰器。
- Python 中的私有和受保护属性:用法、约定和限制。
本章的新内容
我在本章的第二段添加了一个新的题词和几句话来强调“Pythonic”的概念——这只是在第一版的最后讨论过。
“Formatted Displays”已更新以提及 Python 3.6 中引入的 f -string。这是一个很小的变化,因为 f-strings 支持与 format() 内置格式和 str.format() 方法相同的格式化微语言,所以任何以前实现的 __format__ 方法都可以与 f-strings 一起使用。
本章的其余部分几乎没有变化——特殊方法自 Python 3.0 以来基本相同,核心思想出现在 Python 2.2 中。
让我们从使用对象表示方法开始。
对象表现形式
每一种面向对象的语言都至少有一种从任何对象获取字符串表示的标准方法。 Python提供了两个方式:
repr()
返回一个表示开发人员希望看到的对象的字符串。当 Python 控制台或调试器显示一个对象时,你会得到它。
str()
返回一个表示用户希望看到的对象的字符串。这就是你在 print() 方法中传入一个对象时得到的。
正如我们在第 1 章中看到的,特殊方法 __repr__ 和 __str__ 用于支持 repr() 和 str()。
还有两个额外的特殊方法来支持对象的替代表示:__bytes__ 和 __format__。__bytes__ 方法类似于 __str__:它由 bytes() 调用以获取表示为字节序列的对象。关于 __format__,它被 f-strings、内置函数 format() 和 str.format() 方法使用。他们调用 obj.__format__(format_spec) 来获取使用特殊格式代码的对象的字符串显示。我们将在下一个示例中介绍 __bytes__,然后介绍 __format__。
再谈向量类
为了演示用于生成对象表示的许多方法,我们将使用一个类,它类似于我们在第 1 章中看到的 Vector2d 类。我们将在本节和以后的部分中以此为基础。示例 11-1 说明了我们期望 Vector2d 实例的基本行为。
例 11-1。 Vector2d 实例有几种表示形式
>>> v1 = Vector2d(3, 4)
>>> print(v1.x, v1.y) 1
3.0 4.0
>>> x, y = v1 2
>>> x, y
(3.0, 4.0)
>>> v1 3
Vector2d(3.0, 4.0)
>>> v1_clone = eval(repr(v1)) 4
>>> v1 == v1_clone 5
True
>>> print(v1) 6
(3.0, 4.0)
>>> octets = bytes(v1) 7
>>> octets
b'd\\x00\\x00\\x00\\x00\\x00\\x00\\x08@\\x00\\x00\\x00\\x00\\x00\\x00\\x10@'
>>> abs(v1) 8
5.0
>>> bool(v1), bool(Vector2d(0, 0)) 9
(True, False)
- Vector2d 的组件可以作为属性直接访问(没有 getter 方法调用)。
- Vector2d 可以解包为变量元组。
- Vector2d 的 repr 模拟用于构造实例的源代码。
- 在这里使用 eval 表明 Vector2d 调用repr的结果是其构造方法的准确表示。
- Vector2d 支持与 == 比较;这对测试很有用
- print 函数调用 str函数,它为 Vector2d 生成有序对显示。
-
bytes函数使用 __bytes__ 方法生成二进制表示。
-
abs 函数使用 __abs__ 方法返回 Vector2d 的模。
-
bool 函数使用 __bool__ 方法,模为0的 Vector2d 实例返回 False,否则返回 True。
示例 11-1 中的 Vector2d 在 vector2d_v0.py(示例 11-2)中实现。代码基于示例 1-2,除了 + 和 * 操作的方法,我们将在后面的第 16 章中看到。我们将添加 == 的方法,因为它对测试很有用。此时,Vector2d 使用几种特殊方法来提供 Pythonista 在设计良好的对象中期望的操作。
例 11-2。 vector2d_v0.py:到目前为止的方法都是特殊的方法
class Vector2d:
typecode = 'd' 1
def __init__(self, x, y):
self.x = float(x) 2
self.y = float(y)
def __iter__(self):
return (i for i in (self.x, self.y)) 3
def __repr__(self):
class_name = type(self).__name__
return '{}({!r}, {!r})'.format(class_name, *self) 4
def __str__(self):
return str(tuple(self)) 5
def __bytes__(self):
return (bytes([ord(self.typecode)]) + 6
bytes(array(self.typecode, self))) 7
def __eq__(self, other):
return tuple(self) == tuple(other) 8
def __abs__(self):
return math.hypot(self.x, self.y) 9
def __bool__(self):
return bool(abs(self)) 10
- typecode 是我们在 Vector2d 实例与字节之间转换时将使用的类属性。
- 在 __init__ 中将 x 和 y 转换为浮点数可以尽早抛出异常,这在使用不正确的参数调用 Vector2d 时很有帮助。
- __iter__ 使 Vector2d 可迭代;这就是解包工作的原因(例如,x, y = my_vector)。我们只是通过使用生成器表达式来一个接一个地生成组件来实现它。
- __repr__ 通过使用 {!r} 获取各个组件的repr并插值来构建字符串;因为 Vector2d 是可迭代的,*self 将 x 和 y 组件提供给format函数。
- 从可迭代的 Vector2d 中,很容易构建一个元组以显示为有序对。
- 为了生成字节,我们将类型代码转换为字节并连接……
ord()函数主要用来返回对应字符的ascii码;chr()主要用来表示ascii码对应的字符,可以用十进制,也可以用十六进制。
- ...从通过迭代实例构建的array转换而来的bytes。
- 要快速比较所有组件,请从操作数中构建元组。这适用于作为 Vector2d 实例的操作数,但存在问题。请参阅以下警告。
- 模是由 x 和 y 分量构成的直角三角形的斜边的长度。
- __bool__ 使用 abs(self) 计算模,然后将其转换为 bool,因此 0.0 返回 False,非零值返回 True。
Warning:
示例 11-2 中的 __eq__ 方法适用于 Vector2d 操作数,但在将 Vector2d 实例与其他持有相同数值的可迭代对象(例如 Vector(3, 4) == [3, 4])进行比较时,也会返回 True。这可能被视为功能或错误。进一步的讨论需要等到第 16 章,当我们讨论运算符重载时。
我们有一套相当完整的基本方法,但我们仍然需要一种方法来从 bytes() 生成的二进制表示重建 Vector2d。
替代构造函数
由于我们可以将 Vector2d 导出为字节序列,自然我们需要一种从二进制序列中导入 Vector2d 的方法。查看标准库以获取灵感,我们发现 array.array 有一个名为 .frombytes 的类方法符合我们的目的——我们在 “Arrays”中看到了它。我们在 vector2d_v1.py 中的 Vector2d 的类方法中采用它的名称并使用它的功能(示例 11-3)。
例 11-3。 vector2d_v1.py 的一部分:此代码段仅显示了 frombytes 类方法,已添加到 vector2d_v0.py 中的 Vector2d 定义(示例 11-2)
@classmethod 1
def frombytes(cls, octets): 2
typecode = chr(octets[0]) 3
memv = memoryview(octets[1:]).cast(typecode) 4
return cls(*memv) 5
- classmethod 装饰器修饰了该方法,以便可以直接在类上调用它。
- 不传入self参数;相反,类本身作为第一个参数传递——通常命名为 cls。
- 从第一个字节读取typecode。
- 从octets二进制字节序列创建一个memoryview,并使用typecode对其进行转换。
- 将转换产生的memoryview解包到构造函数所需的一对参数中。
我刚刚使用了 classmethod 装饰器,它是 Python所特有的,所以让我们谈谈它。
classmethod 和 staticmethod
Python 教程中没有提到 classmethod 装饰器,也没有提到staticmethod。任何在 Java 中学习过面向对象的人都可能想知道为什么 Python 有这两种装饰器,而不是只有其中一种。
让我们从classmethod开始。示例 11-3 展示了它的用途:定义一个对类而不是实例进行操作的方法。classmethod 改变了方法的调用方式,因此它接收类本身作为第一个参数,而不是一个实例。它最常见的用途是用于替代构造函数,例如示例 11-3 中的 frombytes。请注意 frombytes 的最后一行实际上是如何通过调用 cls 参数来构建新实例来使用它的:cls(*memv)。
相比之下,staticmethod 装饰器更改了方法的调用方式,使其不接收特殊的第一个参数。本质上&#