Python如何设计面向对象的类(下)

代码如下:

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): return str(tuple(self)) def __bytes__(self): return (bytes([ord(self.typecode)]) + bytes(self._components)) def __eq__(self, other): return tuple(self) == tuple(other) def __abs__(self): return math.sqrt(sum(x * x for x in self)) def __bool__(self): return bool(abs(self)) @classmethod def frombytes(cls, octets): typecode = chr(octets[0]) memv = memoryview(octets[1:]).cast(typecode) return cls(memv) # 因为构造函数入参是数组,所以不用再使用*拆包了

其中的reprlib.repr()函数用于生成大型结构或递归结构的安全表达形式,比如:

>>> Vector([3.1, 4.2]) Vector([3.1, 4.2]) >>> Vector((3, 4, 5)) Vector([3.0, 4.0, 5.0]) >>> Vector(range(10)) Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...])

超过6个的元素用...来表示。

第2版:支持切片

Python协议是非正式的接口,只在文档中定义,在代码中不定义。比如Python的序列协议只需要__len__和__getitem__两个方法,Python的迭代协议只需要__getitem__一个方法,它们不是正式的接口,只是Python程序员默认的约定。

切片是序列才有的操作,所以Vector类要实现序列协议,也就是__len__和__getitem__两个方法,代码如下:

def __len__(self): return len(self._components) def __getitem__(self, index): cls = type(self) # 获取实例所属的类 if isinstance(index, slice): # 如果index是slice切片对象 return cls(self._components[index]) # 调用构造方法,返回新的Vector实例 elif isinstance(index, numbers.Integral): # 如果index是整型 return self._components[index] # 直接返回元素 else: msg = '{cls.__name__} indices must be integers' raise TypeError(msg.format(cls=cls))

测试一下:

>>> v7 = Vector(range(7)) >>> v7[-1] # <1> 6.0 >>> v7[1:4] # <2> Vector([1.0, 2.0, 3.0]) >>> v7[-1:] # <3> Vector([6.0]) >>> v7[1,2] # <4> Traceback (most recent call last): ... TypeError: Vector indices must be integers 第3版:动态存取属性

通过实现__getattr__和__setattr__,我们可以对Vector类动态存取属性。这样就能支持v.my_property = 1.1这样的赋值。

如果使用__setitem__方法,那么只能支持v[0] = 1.1。

代码如下:

shortcut_names = 'xyzt' # 4个分量属性名 def __getattr__(self, name): cls = type(self) # 获取实例所属的类 if len(name) == 1: # 只有一个字母 pos = cls.shortcut_names.find(name) if 0 <= pos < len(self._components): # 落在范围内 return self._components[pos] msg = '{.__name__!r} object has no attribute {!r}' # <5> raise AttributeError(msg.format(cls, name)) def __setattr__(self, name, value): cls = type(self) if len(name) == 1: if name in cls.shortcut_names: # name是xyzt其中一个不能赋值 error = 'readonly attribute {attr_name!r}' elif name.islower(): # 小写字母不能赋值,防止与xyzt混淆 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) # 其他name可以赋值

值得说明的是,__getattr__的机制是:对my_obj.x表达式,Python会检查my_obj实例有没有名为x的属性,如果有就直接返回,不调用__getattr__方法;如果没有,到my_obj.__class__中查找,如果还没有,才调用__getattr__方法

正因如此,name是xyzt其中一个时才不能赋值,否则会出现下面的奇怪现象:

>>> v = Vector([range(5)]) >>> v.x = 10 >>> v.x 10 >>> v Vector([0.0, 1.0, 2.0, 3.0, 4.0])

对v.x进行了赋值,但实际未生效,因为赋值后Vector新增了一个x属性,值为10,对v.x表达式来说,直接就返回了这个值,不会走我们自定义的__getattr__方法,也就没办法拿到v[0]的值。

第4版:散列

通过实现__hash__方法,加上现有的__eq__方法,Vector实例就变成了可散列的对象。

代码如下:

import functools import operator def __eq__(self, other): return (len(self) == len(other) and all(a == b for a, b in zip(self, other))) def __hash__(self): hashes = (hash(x) for x in self) # 创建一个生成器表达式 return functools.reduce(operator.xor, hashes, 0) # 计算聚合的散列值

其中__eq__方法做了下修改,用到了归约函数all(),比tuple(self) == tuple(other)的写法,能减少处理时间和内存。

zip()函数取名自zipper拉链,把两个序列咬合在一起。比如:

>>> list(zip(range(3), 'ABC')) [(0, 'A'), (1, 'B'), (2, 'C')] 第5版:格式化

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zwsfgw.html