导语:本文章记录了本人在学习Python基础之面向对象篇的重点知识及我的心得,打算入门Python的朋友们能够来一块儿学习并交流。
本文重点:python
一、了解协议的概念以及利用__getitem__和__len__实现序列协议的方法;
二、掌握切片背后的__getitem__;
三、掌握动态访问属性背后的__getattr__和__setattr__;
四、掌握实现可散列对象背后精简的__hash__和__eq__。
注:本文介绍的vector类将二维vector类推广到多维,跟不上本文的朋友能够移步至《编写符合Python风格的对象》先了解二维向量类的编写。编程
首先,须要就n维向量和二维向量的显示、模的计算等差别从新调整。n维向量的设计包括初始化,迭代,输出,向量实例转为字节序列,求模,求布尔值,比较等内容,代码以下:segmentfault
import math import reprlib from array import array class Vector: typecode='d' def __init__(self,components): self._components=array(self.typecode,components) def __str__(self): return str(tuple(self)) def __iter__(self): return iter(self._components) def __repr__(self): classname=type(self).__name__ components=reprlib.repr(self._components) components=components[components.find('['):-1] return "{}({})".format(classname,components) def __eq__(self, other): return tuple(self)==tuple(other) def __abs__(self): return math.sqrt(sum(x*x for x in self)) def __bytes__(self): return (bytes(self.typecode,encoding='utf-8')+ bytes(array(self.typecode,self._components))) def __bool__(self): return bool(abs(self) @classmethod def frombytes(cls,seqs): typecode=chr(seqs[0]) memv=memoryview(seqs[1:]).cast(typecode) return cls(memv)
在Python中建立功能完善的序列类型无需使用继承,只须要实现符合序列协议的__len__和__getitem__,
具体代码实现以下:数组
class Vector: #省略中间代码 def __len__(self): return len(self._components) def __getitem__(self, item): return self._components[item]
在面向对象编程中,协议是非正式的接口,没有强制力。
所以若是知道类的具体使用场景,实现协议中的一部分也能够。例如,为了支持迭代只实现__getitem__方法便可。ide
在对序列切片(slice)的操做中,解释器容许切片省略start,stop,stride中的部分值甚至是所有省略。经过dir(slice)查阅发现,是切片背后的indices在作这个工做。indices方法会整顿存储数据属性的元组,把start,stop,stride都变成非负数,并且都落在指定长度序列的边界内。
例如slice(-3,None,None).indices(5)整顿完毕以后是(2,5,1)这样合理的切片。函数
__getitem__是支持迭代取值的特殊方法。
咱们将上文的__getitem__改形成能够处理切片的方法,改造须要考虑处处理参数是否为合理切片,合理切片的操做结果是产生新的向量实例。学习
def __getitem__(self, index): cls=type(self) if isinstance(index,slice): return cls(self._components[index])#判断参数为切片时返回新的向量实例 elif isinstance(index,numbers.Integral): return self._components[index]#判断参数为数值时返回对应的数值 else: msg="{cls.__name__} indices must be integers" raise TypeError(msg.format(cls=cls))#判断参数不合理时抛出TypeError
n维向量没有像二维向量同样把访问份量的方式直接在__init__中写入,因为传入的维数不肯定没法采起穷举份量的原始方法,为此咱们须要借助__getattr__实现。假设n维向量最多能处理6维向量,访问向量份量的代码实现以下:优化
shortcut_names='xyztpq' def __getattr__(self, name): cls=type(self) if len(name)==1: index=cls.shortcut_names.find(str(name))#若传入的参数在备选份量中可进行后续处理 if 0<=index<len(self._components):#判断份量的位置索引是否超出实例的边界 return self._components[index] else: msg = "{.__name__} doesn't have attribute {!r}" raise AttributeError(msg.format(cls,name))#不支持非法的份量访问,抛出Error。
Tips:代码严谨之处在于传入的参数即便在备选份量之中,也有可能会超出实例的边界,所以涉及到索引和边界须要认真注意这一点。
设计
尽管咱们实现了__getattr__,但事实上目前的n维向量存在行为不一致的问题,先看一段代码:code
v=Vector(range(5)) print(v.y)#输出1.0 v.y=6 print(v.y)#输出6 print(v)#输出(0.0, 1.0, 2.0, 3.0, 4.0)
上面的例子显示咱们能够访问6维向量的y份量,可是问题在于咱们为y份量赋值的改动没有影响到向量实例v。这种行为是不一致的,而且尚未抛出错误使人匪夷所思。本文中咱们但愿向量份量是只读不可变的,也就是说咱们要对修改向量份量这种不当的行为抛出Error。所以须要额外构造__setattr__,代码实现以下:
def __setattr__(self, key, value): cls=type(self) if len(key)==1: if key in self.shortcut_names: error="can't set value to attribute {attr_name!r}" elif key.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=key) raise AttributeError(msg) super().__setattr__(key,value)#在超类上调用__setattr__方法来提供标准行为。
小结:若是定义了__getattr__方法,那么也要定义__setattr__方法,这样才能避免行为不一致。
可散列对象应知足的三个条件在此再也不赘述,对于n维向量类而言须要作两件事将其散列化:
构造思路是将hash()应用到向量中的每一个元素,并用异或运算符进行聚合计算。因为处理的向量维数提升,采用归约函数functools.reduce处理。
import operator from functools import reduce def __hash__(self): hashes=map(hash,self._components) return reduce(operator.xor,hashes)
上文初始给出的比较方法是粗糙的,下面针对两个维数均不肯定的向量进行比较,代码以下:
def __eq__(self, other): if len(self)!=len(other):#数组数量的比较很关键 return False for x,y in zip(self,other): if x!=y: return False return True
数组数量的比较时很关键的,由于zip在比较数量不等的序列时会随着一个输入的耗尽而中止迭代,而且不抛出Error。
回到正题,上述的逻辑关系能够进一步精简。经过all函数能够把for循环替代:
def __eq__(self, other): return len(self)==len(other) and all(x==y for x,y in zip(self,other))
本人更喜欢后者这种简洁且准确的代码书写方式。
理解n维向量的超球面坐标(r,θ1,θ2,θ3,...,θn-1)计算公式须要额外的数学基础,此处的格式化输出在本质上与《编写符合Python风格的对象》中的格式化输出并没有明显区别,此处不做详述,感兴趣的朋友能够查看以下的代码:
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 else: return a def angles(self): return (self.angle(n) for n in range(1, len(self)))#计算全部角坐标并存入生成器表达式中 def __format__(self, fmt_spec=''): if fmt_spec.endswith('h'): # 超球面坐标标识符 fmt_spec = fmt_spec[:-1] coords = itertools.chain([abs(self)],self.angles()) #利用itertools.chain无缝迭代模和角坐标 outer_fmt = '<{}>' else: coords = self outer_fmt = '({})' components = (format(c, fmt_spec) for c in coords) #格式化极坐标的各元素并存入生成器中 return outer_fmt.format(', '.join(components))