《流畅的Python》笔记。本篇是“面向对象惯用方法”的第三篇。本篇将以上一篇中的Vector2d为基础,定义多维向量Vector。python
自定义Vector
类的行为将与Python标准中的不可变扁平序列同样,它将支持以下功能:编程
__len__
和__getitem__
;Vector
实例;本篇还将经过__getattr__
方法实现属性的动态存取(虽然序列类型一般不会这么作),以及穿插讨论一个概念:把协议当作正式接口。咱们将说明协议和鸭子类型之间的关系,以及对自定义类型的影响。bash
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
实现序列协议。ssh
协议和鸭子类型在以前的文章中也有所说起。在面向对象编程中,协议是非正式的接口,只在文档中定义,在代码中不定义。函数
在Python中,只要实现了协议须要的某些方法,其实就算实现了协议,而不必定须要继承。好比只要实现了__len__
和__getitem__
这两个方法,那么这个类就是知足序列协议的,而不须要从什么“序列基类”继承。测试
鸭子类型:和现实中相反,Python中肯定一个东西是否是“鸭子”,不是测它的“DNA”是否是”鸭子“的DNA,而是看这东西像不像只鸭子。只要像”鸭子“,那它就是“鸭子”。好比,只要一个类实现了__len__
和__getitem__
方法,那它就是序列类,而没必要管它是从哪来的;文件类对象也常是鸭子类型。网站
让Vector
变为序列类型,并能正确返回切片:spa
# 代码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
类型。正是为了使切片的类型正确,这里才作了类型判断。.net
上述代码中用到了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', 'stop']
当咱们用dir()
函数获取slice
的属性时,发现它有start
,stop
和step
数据属性,而且还有一个indices
方法,这里重点说说这个indices
方法。它接收一个长度参数len
,并根据这个len
将slice
类型的start
,stop
和step
三个参数正确转换成在长度范围内的非负数,具体用法以下:
# 代码4 >>> slice(None, 10, 2).indices(5) (0, 5, 2) # 将这些烦人的索引通通转换成明确的正向索引 >>> slice(-3, None, None).indices(5) (2, 5, 1)
自定义Vector
类中并无使用这个方法,由于Vector
的底层咱们使用了array.array
数据类型,切片的具体操做不用咱们自行编写。但若是你的类没有这样的底层序列类型作支撑,那么slice.indices
方法将为你节省大量时间。
目前版本的Vector
中,没有办法经过名称访问向量的份量(如v.x
和v.y
),并且如今的Vector
可能存在大量份量。不过,若是能经过单个字母访问前几个份量的话,这样将很方便,也更人性化。如今,咱们想用x
,y
,z
,t
四个字母分别代替v[0]
,v[1]
,v[2]
和v[3]
,但具体作法并非为实例添加这四个属性,而且咱们也不想在运行时实例能动态添加单个字母的属性,更不想实例能经过这四个字母修改Vector
中self._components
的值。换句话说,咱们只想经过这四个字母提供一种较为方便的访问方式,仅此而已。而要实现这样的功能,则须要实现__getattr__
和__setattr__
方法,如下是它们的代码:
# 代码5.1 class Vector: -- snip -- shortcut_name = "xyzt" def __getattr__(self, name): cls = type(self) if len(name) == 1: # 若是属性是单个字母 pos = cls.shortcut_name.find(name) if 0 <= pos < len(self._components): # 判断是否是xyzt中的一个 return self._components[pos] msg = "{.__name__!r} object has no attribute {!r}" # 想要获取其余属性时则抛出异常 raise AttributeError(msg.format(cls, name)) def __setattr__(self, name, value): cls = type(self) if len(name) == 1: # 不容许建立单字母实例属性,即使是x,y,z,t if name in cls.shortcut_name: # 若是name是xyzt中的一个,设置特殊的错误信息 error = "readonly attibute {attr_name!r}" elif name.islower(): # 为小写字母设置特殊的错误信息 error = "can't set attributes 'a' to 'z' in {cls_name!r}" else: error = "" if error: # 当用户试图动态建立属性时抛出异常 msg = error.format(cls_name=cls.__name__, attr_name=name) raise AttributeError(msg) super().__setattr__(name, value)
解释:
__getattr__
方法。简单来讲,对my_obj.x
表达式,Python会检查my_obj
实例有没有名为x
的实例属性;若是没有,则到它所属的类中查找有没有名为x
的类属性;若是仍是没有,则顺着继承树继续查找。若是依然找不到,则会调用my_obj
所属类中定义的__getattr__
方法,传入self
和属性名的字符串形式(如'x'
);__getattr__
和__setattr_
方法通常同时定义,不然对象的行为很容易出现不一致。好比,若是这里只定义__getattr__
方法,则会出现以下尴尬的代码:
# 代码5.2 >>> v = Vector(range(5)) >>> v Vector([0.0, 1.0, 2.0, 3.0, 4.0]) >>> v.x 0.0 >>> v.x = 10 # 按理说这里应该报错才对,由于不容许修改 >>> v.x 10 >>> v # 实际上是v建立了新实例属性x,这也是为何咱们要定义__setattr__ Vector([0.0, 1.0, 2.0, 3.0, 4.0]) # 行为不一致
__slots__
来禁止添加属性,但咱们这里仍然选择实现__setattr__
来实现此功能。__slots__
属性最好只用于节省内存,并且仅在内存严重不足时才用它,别为了秀操做而写一些别人看着很别扭的代码(只写给本身看的除外)。目前这个Vector
是不可散列的,如今咱们来实现__hash__
方法。具体方法和上一篇同样,也是用各个份量的哈希值进行异或运算,因为Vector
的份量可能不少,这里咱们使用functools.reduce
函数来归约异或值。同时,咱们还将改写以前那个简洁版的__eq__
,使其更高效(至少对大型向量来讲更高效):
# 代码6,请自行导入所需的模块 class Vector: -- snip -- def __hash__(self): hashs = (hash(x) for x in self._components) # 先求各个份量的哈希值 return functools.reduce(operator.xor, hashs, 0) # 而后将全部哈希值归约成一个值 def __eq__(self, other): # 不用像以前那样:生成元组只为使用元组的__eq__方法 return len(self) == len(self) and all(a == b for a, b in zip(self, other))
解释:
__hash__
方法实际上执行的是一个映射归约的过程。每一个份量被映射成了它们的哈希值,这些哈希值再归约成一个值;functool.reduce
传入了第三个参数,而且建议最好传入第三个参数。传入第三个参数能避免这个异常:TypeError: reduce() of empty sequence with no initial value
。若是序列为空,第三个参数就是返回值;不然,在归约中它将做为第一个参数;__eq__
方法中先比较两序列的长度并不只仅是一种捷径。zip
函数并行遍历多个可迭代对象,若是其中一个耗尽,它会当即中止生成值,并且不发出警告;
补充一个小知识:
zip
函数和文件压缩没有关系,它的名字取自拉链头(zipper fastener),这个小物件把两个拉链条的链牙要合在一块儿,是否是很形象?
Vector2d
中,当传入'p'
时,以极坐标的形式格式化数据;因为Vector
的维度可能大于2,如今,当传入参数'h'
时,咱们使用球面坐标格式化数据,即'<r, Φ1, Φ2, Φ3>'
。同时,还须要定义两个辅助方法:
angle(n)
,用于计算某个角坐标;angles()
,返回由全部角坐标构成的可迭代对象。至于这两个的数学原理就不解释了。如下是最后要添加的代码:
# 代码7 class Vector: -- snip -- def angle(self, n): r = math.sqrt(sum(x * x for x in self[n:])) a = math.atan2(r, self[n - 1]) if (n == len(self) - 1) and (self[-1] < 0): return math.pi * 2 - a return a def angles(self): return (self.angle(n) for n in range(1, len(self))) def __format__(self, format_spec=""): if format_spec.endswith("h"): # 若是格式说明符以'h'结尾 format_spec = format_spec[:-1] # 格式说明符前面部分保持不变 coords = itertools.chain([abs(self)], self.angles()) # outer_fmt = "<{}>" else: coords = self outer_fmt = "({})" components = (format(c, format_spec) for c in coords) return outer_fmt.format(", ".join(components))
itertools.chain
函数生成生成器表达式,将多个可迭代对象链接成在一块儿进行迭代。关于生成器的更多内容将在之后的文章中介绍。
至此,多维Vector
暂时告一段落。
迎你们关注个人微信公众号"代码港" & 我的网站 www.vpointer.net ~