文章目录
本章包含以下话题:
- 支持用于生成对象其他表示形式的内置函数(如 repr()、bytes(),等等)
- 使用一个类方法实现备选构造方法
- 扩展内置的 format() 函数和 str.format() 方法使用的格式微语言
- 实现只读属性
- 把对象变为可散列的,以便在集合中及作为 dict 的键使用
- 利用 _slots_ 节省内存。
我们将开发一个简单的二维欧几里得向量类型,在这个过程中涵盖上述全部话题。
在实现这个类型的中间阶段,我们会讨论两个概念:
- 如何以及何时使用 @classmethod 和 @staticmethod 装饰器
- Python 的私有属性和受保护属性的用法、约定和局限
我们从对象表示形式函数开始。
9.1 对象表示形式
每门面向对象的语言至少都有一种获取对象的字符串表示形式的标准方式。Python 提供了
两种方式。
- repr()
以便于开发者理解的方式返回对象的字符串表示形式。 - str()
以便于用户理解的方式返回对象的字符串表示形式。
为了给对象提供其他的表示形式,还会用到另外两个特殊方法:_bytes_ 和_format_。_bytes_ 方法与 _str_ 方法类似:bytes() 函数调用它获取对象的字节序列表示形式。而 _format_ 方法会被内置的 format() 函数和 str.format() 方法调用,使用特殊的格式代码显示对象的字符串表示形式。
记住,在 Python 3 中,
- _repr_、_str_ 和 _format_ 都必须返回 Unicode 字符串(str 类型)。
- 只有_bytes_ 方法应该返回字节序列(bytes 类型)
9.2 再谈向量类
from array import array
import math
class Vector2d:
typecode='d'# typecode是类属性
def __init__(self, x, y):
self.x = x
self.y = 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 __abs__(self):
return math.hypot(self.x, self.y) #模是 x 和 y 分量构成的直角三角形的斜边长
def __bool__(self):
return bool(abs(self))
v1 = Vector2d(3, 4)
print(v1.x, v1.y)
x, y = v1
print(x, y)
print(v1)
v1_clone = eval(repr(v1))
print(v1_clone == v1)
print(v1)
octets = bytes(v1)
print(octets)
print(abs(v1))
9.3 备选构造方法
我们可以把 Vector2d 实例转换成字节序列了;同理,也应该能从字节序列转换成Vector2d 实例。在标准库中探索一番之后,我们发现 array.array 有个类方法.frombytes(2.9.1 节介绍过)正好符合需求。下面在 vector2d_v1.py(见示例 9-3)中为Vector2d 定义一个同名类方法。
@classmethod ➊
def frombytes(cls, octets): ➋
typecode = chr(octets[0]) ➌
memv = memoryview(octets[1:]).cast(typecode) ➍
return cls(*memv) ➎
❶ 类方法使用 classmethod 装饰器修饰。
❷ 不用传入 self 参数;相反,要通过 cls 传入类本身。
❸ 从第一个字节中读取 typecode。
❹ 使用传入的 octets 字节序列创建一个 memoryview,然后使用 typecode 转换。
2.9.2 节简单介绍过 memoryview,说明了它的 .cast 方法。
❺ 拆包转换后的 memoryview,得到构造方法所需的一对参数。
9.4 classmethod与staticmethod
先来看 classmethod。示例 9-3 展示了它的用法:定义操作类,而不是操作实例的方法。classmethod 改变了调用方法的方式,因此类方法的第一个参数是类本身,而不是实例。classmethod 最常见的用途是定义备选构造方法,例如示例 9-3 中的
frombytes。注意,frombytes 的最后一行使用 cls 参数构建了一个新实例,即cls(*memv)。按照约定,类方法的第一个参数名为 cls(但是 Python 不介意具体怎么命名)。
staticmethod 装饰器也会改变方法的调用方式,但是第一个参数不是特殊的值。其实,静态方法就是普通的函数,只是碰巧在类的定义体中,而不是在模块层定义。
>>> class Demo:
... @classmethod
... def klassmeth(*args):
... return args
... @staticmethod
... def statmeth(*args):
... return args
...
>>> Demo.klassmeth()
(<class '__main__.Demo'>,)
>>> Demo.statmeth()
()
>>> Demo.klassmeth('spam')
(<class '__main__.Demo'>, 'spam')
9.5 格式化显示
内置的 format() 函数和 str.format() 方法把各个类型的格式化方式委托给相应的.format(format_spec) 方法。format_spec 是格式说明符,它是:format(my_obj, format_spec) 的第二个参数,或者str.format() 方法的格式字符串,{} 里代换字段中冒号后面的部分。
例如:
>>> br1 = 1/2.43
>>> br1
0.4115226337448559
>>> format(br1, '0.4f')
'0.4115'
>>> '1 BRL={rate:0.2f} USD'.format(rate=br1)
'1 BRL=0.41 USD'
格式规范微语言为一些内置类型提供了专用的表示代码。比如,b 和 x 分别表示二进制和十六进制的 int 类型,f 表示小数形式的 float 类型,而 % 表示百分数形式:
>>> format(42,'b')
'101010'
>>> format(2/3, '.1%')
'66.7%'
下面是内置的 format() 函数和 str.format() 方法的几个示例
>>> from datetime import datetime
>>> now= datetime.now()
>>> format(now, '%H:%M:%S')
'18:35:23'
>>> "Its now {:%I:%M %p}".format(now)
'Its now 06:35 PM'
如果类没有定义 format 方法,从 object 继承的方法会返回 str(my_object)。我
们为 Vector2d 类定义了 str 方法,因此可以这样做:
>>> v1 = Vector2d(3, 4)
>>> format(v1)
'(3.0, 4.0)'
然而,如果传入格式说明符,object.format 方法会抛出 TypeError:
>>> format(v1, '.3f')
Traceback (most recent call last):
...
TypeError: non-empty format string passed to object.__format__
我们将实现自己的微语言来解决这个问题。首先,假设用户提供的格式说明符是用于格式
化向量中各个浮点数分量的。我们想达到的效果是:
>>> v1 = Vector2d(3, 4)
>>> format(v1)
'(3.0, 4.0)'
>>> format(v1, '.2f')
'(3.00, 4.00)'
>>> format(v1, '.3e')
'(3.000e+00, 4.000e+00)'
实现这种输出的 format 方法如示例 9-5 所示。
示例 9-5 Vector2d._format_ 方法,第 1 版
# 在Vector2d类中定义
def __format__(self, fmt_spec=''):
components = (format(c, fmt_spec) for c in self)
return '({}, {})'.format(*components)
对极坐标来说,我们已经定义了计算模的 abs 方法,因此还要定义一个简单的
angle 方法,使用 math.atan2() 函数计算角度。angle 方法的代码如下:
# 在Vector2d类中定义
def angle(self):
return math.atan2(self.y, self.x)
这样便可以增强 format 方法,计算极坐标,如示例 9-6 所示。
示例 9-6 Vector2d.format 方法,第 2 版,现在能计算极坐标了
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)
9.6 可散列的Vector2d
按照定义,目前 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 方法,前面已经实现了)。
什么是可散列的数据类型
可散列的(hashable)
在散列值永不改变,而且如果 a == b,那么 hash(a) == hash(b) 也是 True 的情况下,如果对象既有 _hash_ 方法,也有 _eq_ 方法,那么这样的对象称为可散列的对象。在内置的类型中,大多数不可变的类型都是可散列的;但是,仅当元组的每一个元素都是可散列的时,元组才是可散列的。
- 如果一个对象是可散列的,那么在这个对象的生命周期中,它的散列值是不变的,而且这个对象需要实现 _hash_() 方法。另外可散列对象还要有_eq_() 方法,这样才能跟其他键做比较。如果两个可散列对象是相等的,那么它们的散列值一定是一样的……
- 原子不可变数据类型(str、bytes 和数值类型)都是可散列类型,frozenset 也是可散列的,因为根据其定义,frozenset 里只能容纳可散列类型。元组的话,只有当一个元组包含的所有元素都是可散列类型的情况下,它才是可散列的。
有这么一句话“Python 里所有的不可变类型都是可散列的”。这个说法其实是不准确的,比如虽然元组本身是不可变序列,它里面的元素可能是其他可变类型的引用。
一般来讲用户自定义的类型的对象都是可散列的,散列值就是它们的 id() 函数的返回值,所以所有这些对象在比较的时候都是不相等的。如果一个对象实现了 _eq_ 方法,并且在方法中用到了这个对象的内部状态的话,那么只有当所有这些内部状态都是不可变的情况下,这个对象才是可散列的。
9.6 可散列的Vector
from array import array
import math
class Vector2d:
typecode='d'# typecode是类属性
def __init__(self, x, y):
self.__x = float(x) #使用两个前导下划线(尾部没有下划线,或者有一个下划线),把属性标记为私有
self.__y = float(y)
@property # @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 __abs__(self):
return math.hypot(self.x, self.y) #模是 x 和 y 分量构成的直角三角形的斜边长
def __bool__(self):
return bool(abs(self))
v1 = Vector2d(3, 4)
注意,我们让这些向量不可变是有原因的,因为这样才能实现 hash 方法。这个方法应该返回一个整数,理想情况下还要考虑对象属性的散列值(eq 方法也要使用),因为相等的对象应该具有相同的散列值。
要想创建可散列的类型,不一定要实现特性,也不一定要保护实例属性。只需正确地实现 hash 和 eq 方法即可。但是,实例的散列值绝不应该变化,因此我们借机提到了只读特性。
如果定义的类型有标量数值,可能还要实现 int 和 float 方法(分别被 int()和 float() 构造函数调用),以便在某些情况下用于强制转换类型。此外,还有用于支持内置的 complex() 构造函数的 complex 方法。Vector2d 或许应该提供
complex 方法。
9.7 Python的私有属性和“受保护的”属性
Python 不能像 Java 那样使用 private 修饰符创建私有属性,但是 Python 有个简单的机制,能避免子类意外覆盖“私有”属性。
举个例子。有人编写了一个名为 Dog 的类,这个类的内部用到了 mood 实例属性,但是没有将其开放。现在,你创建了 Dog 类的子类:Beagle。如果你在毫不知情的情况下又创建了名为 mood 的实例属性,那么在继承的方法中就会把 Dog 类的 mood 属性覆盖掉。这是个难以调试的问题。
为了避免这种情况,如果以__mood 的形式(两个前导下划线,尾部没有或最多有一个下划线)命名实例属性,Python 会把属性名存入实例的__dict__ 属性中,而且会在前面加上一个下划线和类名。因此,对 Dog 类来说,__mood 会变成 _Dog__mood;对 Beagle类来说,会变成 _Beagle__mood。这个语言特性叫名称改写(name mangling)。
示例 9-10 私有属性的名称会被“改写”,在前面加上下划线和类名
>>> v1 = Vector2d(3, 4)
>>> v1.__dict__
{'_Vector2d__y': 4.0, '_Vector2d__x': 3.0}
>>> v1._Vector2d__x
3.0
名称改写是一种安全措施,不能保证万无一失:它的目的是避免意外访问,不能防止故意做错事。
9.8 使用 slots 类属性节省空间
默认情况下,Python 在各个实例中名为 dict 的字典里存储实例属性。如 3.9.3 节所述,为了使用底层的散列表提升访问速度,字典会消耗大量内存。如果要处理数百万个属性不多的实例,通过 slots 类属性,能节省大量内存,方法是让解释器在元组中存储实例属性,而不用字典。
定义 slots 的方式是,创建一个类属性,使用 slots 这个名字,并把它的值设为一个字符串构成的可迭代对象,其中各个元素表示各个实例属性。我喜欢使用元组,因为这样定义的 slots 中所含的信息不会变化。