当前位置: 首页 > 科技观察

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

时间:2023-03-15 20:52:19 科技观察

本文将在上一篇二维向量Vector2d类的基础上定义表示多维向量的Vector类。版本一:兼容Vector2d类代码如下:fromarrayimportarrayimportreprlibimportmathclassVector:typecode='d'def__init__(self,components):self._components=array(self.typecode,components)#多维向量存储数组def__iter__(self):returniter(self._components)#构建迭代器def__repr__(self):components=reprlib.repr(self._components)#有限长度表示components=components[components.find('['):-1]return'Vector({})'.format(components)def__str__(self):returnstr(tuple(self))def__bytes__(self):return(bytes([ord(self.typecode)])+bytes(self._components))def__eq__(self,other):returntuple(self)==tuple(other)def__abs__(self):returnmath.sqrt(sum(x*xforxinself))def__bool__(self):returnbool(abs(self))@classmethoddeffrombytes(cls,octets):typecode=chr(octets[0])memv=memoryview(octets[1:]).cast(typecode)returncls(memv)#因为构造函数的入参是数组,所以不需要使用reprlib.repr()函数中*解压Safeexp用于生成大型结构或递归结构的ression,例如:>>>Vector([3.1,4.2])Vector([3.1,4.2])>>>Vector((3,4,5))Vector([3.0,4.0,5.0])>>>矢量(范围(10))矢量([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):returnlen(self._components)def__getitem__(self,index):cls=type(self)#获取实例所属的类ifisinstance(index,slice):#如果index是aslicesliceobjectreturncls(self._components[index])#调用构造函数返回一个新的Vector实例elifisinstance(index,numbers.Integral):#如果index是整数returnself._components[index]#直接返回元素else:msg='{cls.__name__}indicesmustbeintegers'raiseTypeError(msg.format(cls=cls))测试一下:>>>v7=Vector(range(7))>>>v7[-1]#<1>6.0>>>v7[1:4]#<2>向量([1.0,2.0,3.0])>>>v7[-1:]#<3>向量([6.0])>>>v7[1,2]#<4>Traceback(mostrecentcalllast):...TypeError:VectorindicesmustbeintegersVersion3:动态访问属性是通过__getattr__实现的,通过__setattr__,我们可以动态访问Vector类的属性。这支持像v.my_property=1.1这样的赋值。如果使用__setitem__方法,则只支持v[0]=1.1。代码如下:shortcut_names='xyzt'#4个组件属性名def__getattr__(self,name):cls=type(self)#获取实例所属的类iflen(name)==1:#只有一个字母pos=cls.shortcut_names.find(name)if0<=posraiseAttributeError(msg.format(cls,name))def__setattr__(self,name,value):cls=type(self)iflen(name)==1:ifnameincls.shortcut_names:#nameisoneofxyztthatcannot被赋值error='readonlyattribute{attr_name!r}'elifname.islower():#不能分配小写字母以防止与xyzt混淆error="can'tsetattributes'a'to'z'in{cls_name!r}"else:error=''iferror:msg=error.format(cls_name=cls.__name__,attr_name=name)raiseAttributeError(msg)super().__setattr__(name,value)#Othernamescanbeassignedvalue值得注意的是__getattr__的机制是:formy_obj.x表达式,Python会检查my_obj实例是否有名为x的属性,如果有则直接返回,不调用__getattr__方法;如果没有,它将在my_obj.__class__中查找,如果没有,则调用__getattr__方法。正因为如此,当name为xyzt之一时不能赋值,否则会出现如下怪现象:>>>v=Vector([range(5)])>>>v.x=10>>>v。x10>>>vVector([0.0,1.0,2.0,3.0,4.0])给v.x赋值,但是实际上并没有生效,因为赋值之后,Vector增加了一个新的x属性,值为10。对于v.x的表达式,直接返回这个值后,不会使用我们自定义的__getattr__方法,也就没有办法获取到v[0]的值了。版本4:散列通过实施__hash__方法,除了现有的__eq__方法之外,Vector实例成为可散列的对象。代码如下:importfunctoolsimportoperatordef__eq__(self,other):return(len(self)==len(other)andall(a==bfora,binzip(self,other))def__hash__(self):hashes=(hash(x)forxinself)#创建生成器表达式returnfunctools.reduce(operator.xor,hashes,0)#计算聚合的哈希值其中修改了__eq__方法,使用归约函数all(),即优于tuple(self)==tuple(other)可以减少处理时间和内存。zip()函数以拉链命名,它将两个序列对齐在一起。例如:>>>list(zip(range(3),'ABC'))[(0,'A'),(1,'B'),(2,'C')]版本5:格式化Vector的格式类似于Vector2d。它们都定义了__format__方法,但计算方法由极坐标改为球坐标:defangle(self,n):r=math.sqrt(sum(x*xforxinself[n:]))a=math.atan2(r,self[n-1])if(n==len(self)-1)and(self[-1]<0):returnmath.pi*2-aelse:returndefangles(self):return(self.angle(n)forninrange(1,len(self)))def__format__(self,fmt_spec=''):iffmt_spec.endswith('h'):#hypersphericalcoordinatesfmt_spec=fmt_spec[:-1]coords=itertools.chain([abs(self)],self.angles())outer_fmt='<{}>'else:coords=selfouter_fmt='({})'components=(format(c,fmt_spec)forcincoords)returnouter_fmt什么是.format(','.join(components))极坐标和球坐标?不知道,直接跳过总结经过上下两篇的介绍,我们知道了一个Python风格的类是什么样子的。不同于常规的面向对象设计,Python类通过魔术方法实现了Python协议,这样Python类就可以享受到语法糖,不需要通过get和set来编写代码。