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

这一章我们定义自己的类,而且让类的行为跟真正的 Python 对象一样,这一章延续第一章,说明如何实现在很多 Python 类型中常见的特殊方法。

本章包含以下话题:

  • 支持用于生成对象其他表示形式的内置函数(如 repr(), bytes() 等等)
  • 使用一个类方法实现备选构造方法
  • 扩展内置的 format() 函数和 str.format() 方法使用的格式微语言
  • 实现只读属性
  • 把对象变成可散列的,以便在集合中及作为 dict 的键使用
  • 利用 __slots__ 节省内存

我们将开发一个二维欧几里得向量模型,这个过程中覆盖上面所有话题。这个过程中我们会讨论两个概念

  • 如何以及何时利用 @classmethod 和 @staticmethod 装饰器
  • Python 的私有属性和受保护属性的用法,约定和局限

对象表示形式

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

repr(): 便于开发者理解的方式返回对象的字符串表示形式

str(): 便于用户理解的方式返回对象的字符串表示形式

为了给对象提供其他的表现形式,还会用到两个特殊的方法, __bytes____format____bytes__ 方法与 __str__方法类似:bytes() 函数调用它获取对象的字节序列表示形式。而 __format__ 方法会被内置的 format() 和 str.format() 调用。使用特殊的格式代码显示对象的字符串表示形式。

注意:Python3 中 __repr__, __str__, __format__ 方法都必须返回 Unicode 字符串(str)类型。只有 __bytes__ 方法应该返回字节序列(bytes 类型)

再谈向量类

为了说明用于生成对象表示形式的众多方法,我们将使用一个 Vector2d 类,与第一章的类似。这几节会不断完善这个类,我们期望这个类行为如下所示:


In [4]:
v1 = Vector2d(3, 4)
print(v1.x, v1.y) # 可以直接通过属性访问


3.0 4.0

In [5]:
x, y = v1 # 可以拆包成元祖
x, y


Out[5]:
(3.0, 4.0)

In [6]:
v1


Out[6]:
Vector2d(3.0,4.0)

In [7]:
v1_clone = eval(repr(v1)) # repr 函数调用 Vector2d 实例,结果类似于构建实例的源码
v1 == v1_clone # 支持 == 比较


Out[7]:
True

In [8]:
print(v1) # 会调用 str 函数,对 Vector2d 来说,输出的是一个有序对


(3.0, 4.0)

In [9]:
octets = bytes(v1) # 调用 __bytes__ 方法,生成实例的二进制表示形式
octets


Out[9]:
b'd\x00\x00\x00\x00\x00\x00\x08@\x00\x00\x00\x00\x00\x00\x10@'

In [10]:
abs(v1) # 会调用 __abs__ 方法,返回 Vector2d 实例的模


Out[10]:
5.0

In [11]:
bool(v1), bool(Vector2d(0, 0)) # 会调用 __bool__ 方法,判断 Vector2d 的实例的向量长度


Out[11]:
(True, False)

In [12]:
from array import array
import math

class Vector2d:
    typecode = 'd' # 类属性,Vector2d 实例和字节序之间转换使用
    
    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)) # 将 Vector2d 变成可迭代对象,这样才可以拆包
    
    def __repr__(self):
        class_name = type(self).__name__
         # {!r} 获取各个分量的表示形式,然后插值,构成一个字符串。因为 Vector2d 是可迭代对象,所以用 *self 会把 x 和 y 分量提供给 format 函数
        return '{}({!r},{!r})'.format(class_name, *self)
    
    def __str__(self):
        return str(tuple(self))
    
    def __bytes__(self):
        # 我们使用 typecode 转换成字节序列然后返回
        return (bytes([ord(self.typecode)]) +
                bytes(array(self.typecode, self)))
    
    def __eq__(self, other):
        #为了快速比较所有分量,在操作数中构建元祖,对 Vector2d 实例来说,这样做还有问题,看下面的警告
        return tuple(self) == tuple(other) 
    
    def __abs__(self):
        return math.hypot(self.x, self.y)
    
    def __bool__(self):
        return bool(abs(self))

上面的 __eq__ 方法,在两个操作数都是 Vector2d 的时候可用,不过拿 Vector2d 实例和其他具有相同数值的可迭代对象相比,结果也是 True(如 Vector2d(3, 4) == [3, 4])。这个行为可以视为特性,也可以视为缺陷在第 13 章运算符重载时候进一步讨论

我们已经定义了许多基本方法,但是显然少了一个操作:使用 bytes() 函数生成的二进制重建 Vecotr2d 实例

备选构造方法

我们可以把 Vector2d 实例转成字节序列;同理,也应该能从字节序列转换成 Vector2d 实例。在标准库中探索一番之后,我们发现 array.array 有个类方法 .frombytes(2.91 章介绍过,从文件读取数据) 正好符合需求。下面为 Vector2d 定义一个同名的类方法


In [13]:
from array import array
import math

class Vector2d:
    typecode = 'd' 
    
    def __init__(self, x, y):
        self.x = float(x) 
        self.y = float(y)
        
    @classmethod # 类方法使用 @classmethod 装饰器
    def frombytes(cls, octets): # 不用传入 self 参数,相反,要通过 cls 传入类本身
        typecode = chr(octets[0]) #从第一个字节中读取 typecode
        # 用传入的 octets 字节序列创建一个 memoryview,然后使用 typecode 转换
        memv = memoryview(octets[1:]).cast(typecode) # 2.92 章介绍了 cast 方法,将一段内存转换成指定的类型,d 代表 float
        return cls(*memv) #拆包转换后的 memoryview,得到构造方法所需的一对参数

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

In [14]:
v1 = Vector2d(3, 4)
octets = bytes(v1)
print(octets)
v2 = Vector2d.frombytes(octets)
v2


b'd\x00\x00\x00\x00\x00\x00\x08@\x00\x00\x00\x00\x00\x00\x10@'
Out[14]:
Vector2d(3.0,4.0)

我们上面用的 classmethod 装饰器是 Python 专用的,下面解释一下

classmethod 和 staticmethod

我们来看一下 classmethod 装饰器,上面已经展示了它的用法,定义操作类,而不是操作实例的方法。classmethod 改变了调用方法的方式,因此类方法的第一个参数是类本身,而不是实例。classmethod 最常用的用途是定义备选构造方法。例如上面的 frombytes,注意,frombytes 最后一行使用 cls 参数构建了一个新实例,即 cls(*memv),按照约定,类方法的第一个参数为 cls(但不是强制的)

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


In [15]:
class Demo:
    @classmethod
    def klassmeth(*args):
        return args
    
    @staticmethod
    def statmeth(*args):
        return args

Demo.klassmeth()


Out[15]:
(__main__.Demo,)

In [16]:
Demo.klassmeth('kaka')


Out[16]:
(__main__.Demo, 'kaka')

In [17]:
Demo.statmeth()


Out[17]:
()

In [18]:
Demo.statmeth('kaka')


Out[18]:
('kaka',)

不管怎么调用 class.klassmeth,它的第一个参数都是 Demo 类。而 Demo.statmeth 的行为和普通函数类似。一般情况下,都是使用 classmethod,staticmethod 不是特别有用

格式化显示

内置的 format() 函数和 str.format() 方法把各个类型的格式化方式委托给相应的 .__format__(format_spec) 方法。format_spec 是格式说明符,它是:

  • format(my_obj, format_spec) 的第二个参数
  • str.format() 方法的格式字符串,{} 里代换字段中冒号后面的部分

In [20]:
brl =  1 / 2.43
brl


Out[20]:
0.4115226337448559

In [21]:
format(brl, '0.4f')


Out[21]:
'0.4115'

In [22]:
'1 BRL = {rate:0.2f} USD'.format(rate = brl)


Out[22]:
'1 BRL = 0.41 USD'

'{0.mass:5.3e}' 这样的格式字符串其实包含两部分,冒号左边的 0.mass 在代换字段语法中是字段名,冒号后面的 5.3e 是格式说明符。如果对这些陌生的话,先学 format() 函数,掌握格式规范微语言,然后再阅读格式字符串语法("Format String Syntax",https://docs.python.org/3/library/string.html#formatspec)。学习 str.format() 方法使用的 {:} 代换字段表示法(包含转换标志 !s, !r, !a)

格式规范微语言为一些内置类型提供了专用的表示代码。例如,b 和 x 分别代表 二进制和十六进制的 int 类型。f 表示 float 类型,而 % 表示百分数形式


In [23]:
format(42, 'b')


Out[23]:
'101010'

In [24]:
format(2 / 3, '.1%')


Out[24]:
'66.7%'

格式规范微语言是可扩展的,因为各个类可以自行决定如何解释 format_spec 参数。例如,datetime 模块中的类,它们的 __format__ 方法使用的格式代码与 strftime() 函数一样。下面是内置的 format() 函数和 str.format() 方法的几个示例:


In [25]:
from datetime import datetime
now = datetime.now()
format(now, '%H:%M:%s')


Out[25]:
'11:58:1497067119'

In [26]:
"It's now {:%I:%M %p}".format(now)


Out[26]:
"It's now 11:58 AM"

如果类没有定义 __format__ 方法,从 object 继承的方法会返回 str(my_object)。我们为 Vector2d 类定义了 __str__ 方法,因此可以这样:


In [27]:
v1 = Vector2d(3, 4)
format(v1)


Out[27]:
'(3.0, 4.0)'

然而,传入格式说明符,object.__format__ 方法会抛出 TypeError


In [28]:
format(v1, '.3f')


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-28-5732af69f96b> in <module>()
----> 1 format(v1, '.3f')

TypeError: unsupported format string passed to Vector2d.__format__

我们将实现自己的微语言解决这个问题。首先,假设用户提供的格式说明符是用于格式化向量中各个浮点数分量的。我们想达到的效果如下:


In [36]:
v1 = Vector2d(3, 4)
format(v1)


Out[36]:
'(3.0, 4.0)'

In [37]:
format(v1, '.2f')


Out[37]:
'(3.00, 4.00)'

In [38]:
format(v1, '3e')


Out[38]:
'(3.000000e+00, 4.000000e+00)'

实现这种输出的 __format__ 方法是:


In [19]:
def __format__(self, fmt_spec=''):
    components = (format(c, fmt_spec) for c in self)
    return '({}, {})'.format(*components)

下面要在微语言添加一个自定义格式的代码:如果格式说明以 'p' 为结尾,那么在极坐标中显示向量,即 <r, theta>,其中 r 是 模,theta 是弧度,其他部分('p' 之前的部分像往常一样解释)

注意自定义的格式代码不要和已有的重复,整数使用的有 'bcdoxXn', 浮点数使用的有 'eEfFgGn%', 字符串有的是 's'。所以我们选的极坐标代码是 'p',各个类都有自己的方式解释格式代码,自定义格式代码中重复使用代码字母不会出错,但是可能会让用户迷惑。

对于极坐标来说,我们已经定义了计算模的 __abs__ 方法,因此还要定义一个简单的 angle 方法,使用 math.atan2() 函数计算角度。angle 方法的代码如下:


In [29]:
def angle(self):
    return math.atan2(self.y, self.x)

这样方便增强 __format__ 方法,现在我们可以让其计算极坐标:


In [31]:
def __format__(self, fmt_spec=''):
    if fmt_spec.endswith('p'):
        fmt_spec = fmt_spec[:-1] # 删除 p 后缀
        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)

可散列的 Vector2d

按照定义,我们现在的 Vector2d 是不可散列的,因此不能放入集合 (set) 中:


In [32]:
v1 = Vector2d(3, 4)
hash(v1)


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-32-450ace16a47d> in <module>()
      1 v1 = Vector2d(3, 4)
----> 2 hash(v1)

TypeError: unhashable type: 'Vector2d'

为了将其设成可散列的,必须使用 __hash__ 方法(还需要 __eq__ 方法,前面实现了)。此外,还要让向量不可变。

目前我们可以为分量赋新值,如 v1.x = 7,我们应该让其无法赋值。


In [33]:
class Vector2d:
    typecode = 'd'
    
    def __init__(self, x, y):
        self.__x = float(x) # 有两个前导下划线,或一个,把属性标记为私有的
        self.__y = float(y)
        
    @property               # property 把读值方法标记为特性
    def x(self):            # 读值方法与公开属性名一样,x
        return self.__x
    @property
    def y(self):
        return self.__y
    
    def __iter__(self):
        return (i for i in (self.x, self.y))
    
    # ...下面省略了。

注意,我们让这些向量不可变是有原因的,因为这样才能实现 __hash__ 方法。这个方法应该返回一个整数,理想情况下还要考虑对象属性的散列值(__eq__ 方法也要使用),因为相等的对象应该具有相同的散列值。根据官方文档,最好使用 异或(^)来混合各个分量的散列值


In [34]:
def __hash__(self):
    return hash(self.x) ^ hash(self.y)

如果创建可散列的类型,不一定实现特性,也不一定要保护实例属性。只需要正确的实现 __hash____eq__ 方法即可。但是,实例的散列值绝不应该变化,因此我们借机提到了只读属性。

如果定义的类型有标量数值,可能还要实现 __int____float__ 方法(分别被 int() 和 float() 构造函数调用),以便在某些情况下用于强制类型转换,此外,还有用于支持内置的 complex() 构造函数的 __complex__ 方法。

下面是完整的代码:


In [71]:
from array import array
import math

class Vector2d:
    typecode = 'd' 
    
    def __init__(self, x, y):
        self.__x = float(x) 
        self.__y = float(y)

    @property               # property 把读值方法标记为特性
    def x(self):            # 读值方法与公开属性名一样,x
        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 __hash__(self):
        return hash(self.x) ^ hash(self.y)

    def __abs__(self):
        return math.hypot(self.x, self.y) # 返回欧几里德范数 sqrt(x*x + y*y)。
    
    def __bool__(self):
        return bool(abs(self))

    def angle(self):
        return math.atan2(self.y, self.x)
    
    def __format__(self, fmt_spec=''):
        if fmt_spec.endswith('p'):
            fmt_spec = fmt_spec[:-1] # 删除 p 后缀
            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)

    @classmethod # 类方法使用 @classmethod 装饰器
    def frombytes(cls, octets): # 不用传入 self 参数,相反,要通过 cls 传入类本身
        typecode = chr(octets[0]) #从第一个字节中读取 typecode
        # 用传入的 octets 字节序列创建一个 memoryview,然后使用 typecode 转换
        memv = memoryview(octets[1:]).cast(typecode) # 2.92 章介绍了 cast 方法,将一段内存转换成指定的类型,d 代表 float
        return cls(*memv) #拆包转换后的 memoryview,得到构造方法所需的一对参数

测试:


In [41]:
v1 = Vector2d(3, 4)
print(v1.x, v1.y)


3.0 4.0

In [42]:
x, y = v1
x, y


Out[42]:
(3.0, 4.0)

In [43]:
v1


Out[43]:
Vector2d(3.0,4.0)

In [45]:
v1_clone = eval(repr(v1))
v1 == v1_clone


Out[45]:
True

In [46]:
print(v1)


(3.0, 4.0)

In [47]:
octets = bytes(v1)
octets


Out[47]:
b'd\x00\x00\x00\x00\x00\x00\x08@\x00\x00\x00\x00\x00\x00\x10@'

In [48]:
abs(v1)


Out[48]:
5.0

In [49]:
bool(v1), bool(Vector2d(0, 0))


Out[49]:
(True, False)

In [51]:
v1_clone = Vector2d.frombytes(bytes(v1))
v1_clone


Out[51]:
Vector2d(3.0,4.0)

In [52]:
v1 == v1_clone


Out[52]:
True

In [53]:
format(v1)


Out[53]:
'(3.0, 4.0)'

In [54]:
format(v1, '.2f')


Out[54]:
'(3.00, 4.00)'

In [55]:
format(v1, '.3e')


Out[55]:
'(3.000e+00, 4.000e+00)'

In [56]:
Vector2d(0, 0).angle()


Out[56]:
0.0

In [57]:
Vector2d(1, 0).angle()


Out[57]:
0.0

In [59]:
epsilon = 10 ** -8
abs(Vector2d(0, 1).angle() - math.pi / 2) < epsilon


Out[59]:
True

In [60]:
abs(Vector2d(1, 1).angle() - math.pi / 4) < epsilon


Out[60]:
True

In [61]:
format(Vector2d(1, 1), 'p')


Out[61]:
'<1.4142135623730951, 0.7853981633974483>'

In [62]:
format(Vector2d(1, 1), '.3ep')


Out[62]:
'<1.414e+00, 7.854e-01>'

In [63]:
format(Vector2d(1, 1), '.5fp')


Out[63]:
'<1.41421, 0.78540>'

In [64]:
v1.x, v1.y


Out[64]:
(3.0, 4.0)

In [65]:
v1.x = 123


---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-65-755261bacf40> in <module>()
----> 1 v1.x = 123

AttributeError: can't set attribute

In [66]:
v1 = Vector2d(3, 4)
v2 = Vector2d(3.1, 4.2)
hash(v1), hash(v2)


Out[66]:
(7, 384307168202284039)

In [67]:
len(set([v1, v2]))


Out[67]:
2

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

Python 不能像 Java 一样使用 private 修饰符创建私有属性,但是 Python 有个简单的机制,能避免子类意外覆盖 “私有” 属性

举个例子,如果有人编写了一个 Dog 类,有一个 mood 实例属性,但是没有开放,你创建了 Dog 子类,你在毫不知情的情况下又创建了 mood 的实例属性,那么会在继承方法中会把 Dog 类的 mood 属性覆盖掉。这是难以调试的问题

为了避免这种情况,如果以 __mood 的形式(两个前导下划线,尾部没有或最多有一个前导下划线)命名实例属性,Python 会把属性名存到实例的 __dict__ 属性中,而且会在前面加上一个下划线和类名。因此,对于 Dog 类来说,__mood 会变成 _Dog__mood,对于 Beagle 类来说,会变成 _Beagle__mood。这个语言特性叫名称改写


In [68]:
v1 = Vector2d(3, 4)
v1.__dict__


Out[68]:
{'_Vector2d__x': 3.0, '_Vector2d__y': 4.0}

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

但不是所有 Python 程序员都喜欢这个功能,他们不喜欢两个下划线的这种特别不对称的写法,所以约定使用一个下划线编写受保护的属性(例如 self.x)。Python 不会对单下划线进行特殊处理,不过这是很多 Python 程序员严格遵守的约定,他们不会在外部访问这种属性。

Python 文档的某些角落将使用一个下划线前缀标记为受保护的属性。

总之,Vector2d 的分量都是 “私有的”,而 Vector2d 实例都是 “不可变” 的。这里用了两对引号是因为不能真正实现私有和不可变

使用 __slots__ 类属性节省空间

默认情况下,Python 在各个实例名为 __dict__ 字典里存储实例属性,如第三章所说,为了使用底层的散列表提升访问速度,字典会消耗大量内存,通过 __slots__ 类属性,能节省大量内存,方法是让解释器在元祖中存储实例属性,而不用字典

继承自超类的 __slots__ 属性没有效果。Python 只会使用各个类中定义的 __slots__ 属性。

定义 __slots__ 的方式是,创建一个类属性,使用 __slots__ 这个名字,并把它的值设为一个字符串构成的可迭代对象,其中各个元素表示各个实例属性。我喜欢使用元组,因为这样定义的 __slots__ 所含的信息不会变化,如下所示:


In [69]:
class Vector2d:
    __slots__ = ('__x', '__y')
    # 下面是各个方法,省略

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

如果要处理数百万个数值对象,应该使用 Numpy 数组,它能高效使用内存,而且提供了高度优化的数值处理函数,其中很多都一次操作整个数组

然而,节省的内存也可能被再次吃掉,如果把 __dict__ 这个名称添加到 __slots__ 中,实例会在元祖中保存各个实例的属性。此外还支持动态创建属性,这些属性存储在常规的 __dict__ 中。当然,把 __dict__ 加到 __slots__ 中可能完全违背了初衷,这取决于各个实例的静态属性和动态属性的数量及其用法。粗心的优化甚至比提早优化还糟糕

此外,还有一个实例属性值得注意,__weakref__ 属性,为了让对象支持弱引用,必须有这个属性,用户定义的类中默认就有 __weakref__ 属性。可是如果类中定义了 __slots__ 属性,而且想把实例作为弱引用的目标,就要把 __weakref__ 添加到 __slots__ 中。

综上,__slots__ 属性有些需要注意的地方,而且不能滥用。不能使用它来限制用户能赋值的属性。处理列表中数据时,__slots__ 属性最有用,例如模式固定的数据库记录,大型数据集。然而,如果你经常处理大量数据,一定要看一下 Numpy,此外 Pandas 也值得了解

__slots__ 的问题

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

  • 每个子类都要定义 __slots__ 属性,因为解释器会忽略继承的 __slots__ 属性
  • 每个实例只能拥有 __slots__ 列出来的属性,除非把 __dict__ 加入 __slots__ 中(这样做失去了节省内存的功效)
  • 如果不把 __weakref__ 加入 __slots__,实例就不能作为弱引用的目标

如果你的程序不用处理百万个实例,或许不值得费劲去创建不寻常的类,那就禁止它创建动态属性或者不支持弱引用。与其它优化措施一样,仅当权衡当下的需求并仔细搜集资料后证明确实有必要使用,才使用 __slots__ 属性

覆盖类属性

Python 有一个很独特的特性,类属性可用于为实例属性提供默认值。Vector2d 中有个 typecode 类属性,__bytes__ 方法两次用到了它,而且都故意使用 self.typecode 读取它的值,因为 Vector2d 实例本身没有 typecode 属性,所以 self.typecode 默认获取的是 Vector2d.typecode 的值

但是,如果为不存在的实例属性赋值,会新建实例属性。假如我们为 typecode 实例属性赋值,那么同名类属性不受影响。然而,自此以后,实例读取的 self.typecode 是实例属性 typecode,也就是把同名类属性遮盖了。借助这一特性,可以为各个实例的 typecode 属性定制不同的值。

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


In [72]:
v1 = Vector2d(1.1, 2.2)
dumpd = bytes(v1)
dumpd


Out[72]:
b'd\x9a\x99\x99\x99\x99\x99\xf1?\x9a\x99\x99\x99\x99\x99\x01@'

In [73]:
len(dumpd)


Out[73]:
17

In [74]:
v1.typecode = 'f'
dumpf = bytes(v1)
dumpf


Out[74]:
b'f\xcd\xcc\x8c?\xcd\xcc\x0c@'

In [75]:
len(dumpf)


Out[75]:
9

这就是我们为什么要在字节序列之前加上 typecode 的值的原因:为了支持不同格式

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


In [77]:
Vector2d.typecode = 'f'

然而,有种修改方法更符合 Python 风格,而且效果更持久,也更有针对性。类属性是公开的,因此会被子类继承,于是经常会创建一个子类,只用于定制类的属性。Django 基于类的视图就大量使用了这种技术。就像下面这样:


In [80]:
class ShortVector2d(Vector2d):
    typecode = 'f'

sv = ShortVector2d(1 / 11, 1 / 27)
sv  # 查看 sv 的 repr 形式


Out[80]:
ShortVector2d(0.09090909090909091,0.037037037037037035)

In [81]:
len(bytes(sv))


Out[81]:
9

这也说明了我们在 Vector2d.__repr__ 方法中为什么没有硬编码 class_name 的值,而是使用 type(self).__name__ 获取。因为如果硬编码,子类继承的时候还要重写 __repr__ 方法,这样从实例读取类名,__repr__ 方法就可以放心的继承

至此,我们通过一个简单的类说明了如何利用数据模型处理 Python 的其他功能:提供不同的对象表示形式,实现自定义格式代码,公开只读属性,以及通过 hash() 函数支持集合和映射