得益于 Python 数据模型,自定义类型的行为可以像内置类型那样自然。 实现如此自然的行为,靠的不是继承,而是鸭子类型(duck typing): 我们只需按照预定行为实现对象所需的方法即可。

对象表示形式

每门面向对象的语言至少都有一种获取对象的字符串表示形式的标准方 式。Python 提供了两种方式。

repr()
  以便于开发者理解的方式返回对象的字符串表示形式。
str()
  以便于用户理解的方式返回对象的字符串表示形式。

我们要实现 __repr__ 和 __str__ 特殊方法,为 repr() 和 str() 提供支持。

再谈向量类

>>> 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)

❶ Vector2d 实例的分量可以直接通过属性访问(无需调用读值方 法)。

❷ Vector2d 实例可以拆包成变量元组。

❸ repr 函数调用 Vector2d 实例,得到的结果类似于构建实例的源 码。

❹ 这里使用 eval 函数,表明 repr 函数调用 Vector2d 实例得到的是 对构造方法的准确表述。

❺ Vector2d 实例支持使用 == 比较;这样便于测试。 ❻ print 函数会调用 str 函数,对 Vector2d 来说,输出的是一个有 序对。

❼ bytes 函数会调用 __bytes__ 方法,生成实例的二进制表示形式。

❽ abs 函数会调用 __abs__ 方法,返回 Vector2d 实例的模。

❾ bool 函数会调用 __bool__ 方法,如果 Vector2d 实例的模为零, 返回 False,否则返回 True。

实现

from array import array import math
class Vector2d:    
    typecode = 'd'  ➊
    def __init__(self, x, y):
        self.x = float(x)   ➋
        self.y = float(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)  ➒
    def __bool__(self):
        return bool(abs(self))  ➓

❶ typecode 是类属性,在 Vector2d 实例和字节序列之间转换时使 用。

❷ 在 __init__ 方法中把 x 和 y 转换成浮点数,尽早捕获错误,以防 调用 Vector2d 函数时传入不当参数。

 

❸ 定义 __iter__ 方法,把 Vector2d 实例变成可迭代的对象,这样才 能拆包(例如,x, y = my_vector)。这个方法的实现方式很简单,直接调用生成器表达式一个接一个产出分量。

❹ __repr__ 方法使用 {!r} 获取各个分量的表示形式,然后插值,构 成一个字符串;因为 Vector2d 实例是可迭代的对象,所以 *self 会把 x 和 y 分量提供给 format 函数。

❺ 从可迭代的 Vector2d 实例中可以轻松地得到一个元组,显示为一个 有序对。

❻ 为了生成字节序列,我们把 typecode 转换成字节序列,然后……

❼ ……迭代 Vector2d 实例,得到一个数组,再把数组转换成字节序列。

❽ 为了快速比较所有分量,在操作数中构建元组。对 Vector2d 实例来 说,可以这样做,不过仍有问题。参见下面的警告。

❾ 模是 x 和 y 分量构成的直角三角形的斜边长。

❿ __bool__ 方法使用 abs(self) 计算模,然后把结果转换成布尔 值,因此,0.0 是 False,非零值是 True。

 

classmethod与staticmethod 

classmethod用法:定义操作类,而不是 操作实例的方法。classmethod 改变了调用方法的方式,因此类方法 的第一个参数是类本身,而不是实例。classmethod 最常见的用途是 定义备选构造方法。

staticmethod 装饰器也会改变方法的调用方式,但是第一个参数不是 特殊的值。其实,静态方法就是普通的函数,只是碰巧在类的定义体 中,而不是在模块层定义。

格式化显示 

内置的 format() 函数和 str.format() 方法把各个类型的格式化方式 委托给相应的 .__format__(format_spec) 方法。format_spec 是格 式说明符,它是:format(my_obj, format_spec) 的第二个参数,或者 str.format() 方法的格式字符串,{} 里代换字段中冒号后面的部分。

>>> brl = 1/2.43  # BRL到USD的货币兑换比价 
>>> brl 
0.4115226337448559 
>>> format(brl, '0.4f')  # ➊ 
'0.4115' 
>>> '1 BRL = {rate:0.2f} USD'.format(rate=brl)  # ➋ 
'1 BRL = 0.41 USD'

可散列的Vector2d 

为了把 Vector2d 实例变成可散列的,必须使用 __hash__ 方法(还需 要 __eq__ 方法,前面已经实现了)。此外,还要让向量不可变,为此,我们要把 x 和 y 分量设为只读特性。

让 Vector2d 不可变的代码:

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))  ➏

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

Python的私有属性和“受保护的”属性 

举个例子。有人编写了一个名为 Dog 的类,这个类的内部用到了 mood 实例属性,但是没有将其开放。现在,你创建了 Dog 类的子 类:Beagle。如果你在毫不知情的情况下又创建了名为 mood 的实例属 性,那么在继承的方法中就会把 Dog 类的 mood 属性覆盖掉。这是个难 以调试的问题。
为了避免这种情况,如果以 __mood 的形式(两个前导下划线,尾部没 有或最多有一个下划线)命名实例属性,Python 会把属性名存入实例的 __dict__ 属性中,而且会在前面加上一个下划线和类名。因此,对 Dog 类来说,__mood 会变成 _Dog__mood;对 Beagle 类来说,会变成 _Beagle__mood。这个语言特性叫名称改写。

名称改写是一种安全措施,不能保证万无一失:它的目的是避免意外访 问,不能防止故意做错事。

使用 __slots__ 类属性节省空间 

默认情况下,Python 在各个实例中名为 __dict__ 的字典里存储实例属性。

为了使用底层的散列表提升访问速度,字典会消 耗大量内存。如果要处理数百万个属性不多的实例,通过 __slots__ 类属性,能节省大量内存,方法是让解释器在元组中存储实例属性,而 不用字典。

定义 __slots__ 的方式是,创建一个类属性,使用 __slots__ 这个名 字,并把它的值设为一个字符串构成的可迭代对象,其中各个元素表示 各个实例属性。

class Vector2d:
    __slots__ = ('__x', '__y')
    typecode = 'd'

在类中定义 __slots__ 属性的目的是告诉解释器:“这个类中的所有实 例属性都在这儿了!”这样,Python 会在各个实例中使用类似元组的结 构存储实例变量,从而避免使用消耗内存的 __dict__ 属性。如果有数 百万个实例同时活动,这样做能节省大量内存。

在类中定义 __slots__ 属性之后,实例不能再有 __slots__ 中所列名称之外的其他属性。

然而,“节省的内存也可能被再次吃掉”:如果把 '__dict__' 这个名称 添加到 __slots__ 中,实例会在元组中保存各个实例的属性,此外还 支持动态创建属性,这些属性存储在常规的 __dict__ 中。

__slots__ 的问题

总之,如果使用得当,__slots__ 能显著节省内存,不过有几点要注意。

每个子类都要定义 __slots__ 属性,因为解释器会忽略继承的 __slots__ 属性。

实例只能拥有 __slots__ 中列出的属性,除非把 '__dict__' 加 入 __slots__ 中(这样做就失去了节省内存的功效)。

如果不把 '__weakref__' 加入 __slots__,实例就不能作为弱引 用的目标。

覆盖类属性 

Python 有个很独特的特性:类属性可用于为实例属性提供默认值。

Vector2d.typecode 属性的默认值是 'd',即转换成字节序列时使用 8 字节双精度浮点数表示向量的各个分量。如果在转换之前把 Vector2d 实例的 typecode 属性设为 'f',那么使用 4 字节单精度浮 点数表示各个分量。

如果想修改类属性的值,必须直接在类上修改,不能通过实例修改。如 果想修改所有实例(没有 typecode 实例变量)的 typecode 属性的默 认值,可以这么做:

>>> Vector2d.typecode = 'f'

然而,有种修改方法更符合 Python 风格,而且效果持久,也更有针对 性。类属性是公开的,因此会被子类继承,于是经常会创建一个子类, 只用于定制类的数据属性:

>>> from vector2d_v3 import Vector2d 
>>> class ShortVector2d(Vector2d):  # ➊ 
...     typecode = 'f' 
... 
>>> sv = ShortVector2d(1/11, 1/27)  # ➋ 
>>> sv 
ShortVector2d(0.09090909090909091, 0.037037037037037035)  # ➌
>>> len(bytes(sv))  # ➍ 
9

❶ 把 ShortVector2d 定义为 Vector2d 的子类,只用于覆盖 typecode 类属性。

❷ 为了演示,创建一个 ShortVector2d 实例,即 sv。

❸ 查看 sv 的 repr 表示形式。

❹ 确认得到的字节序列长度为 9 字节,而不是之前的 17 字节