《流畅的Python》笔记。
本篇是“面向对象惯用方法”的第三篇。本篇将以上一篇中的Vector2d为基础,定义多维向量Vector。
1. 前言
自定义
Vector
类的行为将与Python标准中的不可变扁平序列一样,它将支持如下功能:
-
基本的序列协议:
__len__
和__getitem__
; - 正确表述拥有很多元素的实例;
-
适当的切片支持,用于生成新的
Vector
实例; - 综合各个元素的值计算散列值;
- 自定义的格式语言扩展。
本篇还将通过
__getattr__
方法实现属性的动态存取(虽然序列类型通常不会这么做),以及穿插讨论一个概念:把协议当做正式接口。我们将说明协议和
鸭子类型
之间的关系,以及对自定义类型的影响。
2. 初版Vector
Vector
的构造方法将和所有内置序列类型一样,以可迭代对象为参数。如果其中元素过多,
repr()
函数返回的字符串将会使用
...
省略一部分内容,它的初始版本如下:
# 代码1
from array import array
import reprlib
import math
class Vector:
typecode = "d"
def __init__(self, components): # 以可迭代对象为参数
self._components = array(self.typecode, components)
def __iter__(self):
return iter(self._components)
def __repr__(self):
components = reprlib.repr(self._components)
components = components[components.find("["):-1]
return "Vector({})".format(components)
def __str__(self): # 和Vector2d相同
return str(tuple(self))
def __bytes__(self):
return (bytes([ord(self.typecode)]) + bytes(self._components))
def __eq__(self, other): # 和Vector2d相同
return tuple(self) == tuple(other)
def __abs__(self):
return math.sqrt(sum(x * x for x in self))
def __bool__(self): # 和Vector2d相同
return bool(abs(self))
@classmethod
def frombytes(cls, octets):
typecode = chr(octets[0])
memv = memoryview(octets[1:]).cast(typecode)
return cls(memv) # 去掉了Vector2d中的星号*
之所以没有直接继承制
Vector2d
,既是因为这两个类的构造方法不兼容,也是因为我们要为
Vector
实现序列协议。
3. 协议和鸭子类型
协议和鸭子类型在之前的文章中也有所提及。在面向对象编程中, 协议是非正式的接口 ,只在文档中定义,在代码中不定义。
在Python中,只要实现了协议需要的某些方法,其实就算实现了协议,而
不一定需要继承
。比如只要实现了
__len__
和
__getitem__
这两个方法,那么这个类就是满足序列协议的,而不需要从什么“序列基类”继承。
鸭子类型:和现实中相反,Python中确定一个东西是不是“鸭子”,不是测它的“DNA”是不是”鸭子“的DNA,而是看这东西像不像只鸭子。只要像”鸭子“,那它就是“鸭子”。比如,只要一个类实现了
__len__
和
__getitem__
方法,那它就是序列类,而不必管它是从哪来的;文件类对象也常是鸭子类型。
4. 第2版Vector:支持切片
让
Vector
变为序列类型,并能正确返回切片:
# 代码2,将以下代码添加到初版Vector中
class Vector:
-- snip --
def __len__(self):
return len(self._components)
def __getitem__(self, index):
cls = type(self)
if isinstance(index, slice): # 如果index是个切片类型,则构造新实例
return cls(self._components[index])
elif isinstance(index, numbers.Integral): # 如果index是个数,则直接返回
return self._components[index]
else:
msg = "{cls.__name__} indices must be integers"
raise TypeError(msg.format(cls=cls))
如果
__getitem__
函数直接返回切片:
return self._components[index]
,那么得到的数据将是
array
类型,而不是
Vector
类型。正是为了使切片的类型正确,这里才做了类型判断。
上述代码中用到了
slice
类型,它是Python的内置类型,这里顺便补充一下
切片原理
,直接上代码:
# 代码3
>>> class MySeq:
... def __getitem__(self, index):
... return index # 直接返回传给它的值
...
>>> s = MySeq()
>>> s[1]
1 # 单索引,没啥新奇的
>>> s[1:3]
slice(1, 3, None) # 返回来一个slice类型
>>> s[1:10:2]
slice(1, 10, 2) # 注意slice类型的结构
>>> s[1:10:2, 9]
(slice(1, 10, 2), 9) # 如果[]中有逗号,__getitem__收到的是元组
>>> s[1:10:2, 7:9]
(slice(1, 10, 2), slice(7, 9, None))
>>> dir(slice) # 注意最后四个元素
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__',
'__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__',
'__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__',
'__sizeof__', '__str__', '__subclasshook__', 'indices', 'start', 'step'