本文将在上篇文章二维向量Vector2d类的基础上,定义表示多维向量的Vector类。

第1版:兼容Vector2d类

代码如下:

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版:格式化

Vector的格式化跟Vector2d大同小异,都是定义__format__方法,只是计算方式从极坐标换成了球面坐标:

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'): # hyperspherical coordinates
fmt_spec = fmt_spec[:-1]
coords = itertools.chain([abs(self)],
self.angles())
outer_fmt = '<{}>'
else:
coords = self
outer_fmt = '({})'
components = (format(c, fmt_spec) for c in coords)
return outer_fmt.format(', '.join(components))

极坐标和球面坐标是啥?我也不知道,略过就好。

小结

经过上下两篇文章的介绍,我们知道了Python风格的类是什么样子的,跟常规的面向对象设计不同的是,Python的类通过魔法方法实现了Python协议,使Python类在使用时能够享受到语法糖,不用通过get和set的方式来编写代码

参考资料:

《流畅的Python》第10章 序列的修改、散列和切片

Python如何设计面向对象的类(下)的更多相关文章

  1. Python如何设计面向对象的类(上)

    Python是一门高级语言,支持面向对象设计,如何设计一个符合Python风格的面向对象的类,是一个比较复杂的问题,本文提供一个参考,表达一种思路,探究一层原理. 目标 期望实现的类具有以下基本行为: ...

  2. python进阶01 面向对象、类、实例、属性封装、实例方法

    python进阶01 面向对象.类.实例.属性封装.实例方法 一.面向对象 1.什么是对象 #一切皆对象,可以简单地将“对象”理解为“某个东西” #“对象”之所以称之为对象,是因为它具有属于它自己的“ ...

  3. 22.python中的面向对象和类的基本语法

    当我发现要写python的面向对象的时候,我是踌躇满面,坐立不安呀.我一直在想:这个坑应该怎么爬?因为python中关于面向对象的内容很多,如果要讲透,最好是用面向对象的思想重新学一遍前面的内容.这个 ...

  4. python初步学习-面向对象之 类(二)

    方法重写 如果你的父类方法的功能不能满足你的需求,你可以在子类重写你父类的方法: #!/usr/bin/env python #coding:utf8 class Parent: def myMeth ...

  5. python学习之【第十七篇】:Python中的面向对象(类和对象)

    1.什么是类和类的对象? 类是一种数据结构,我们可以用它来定义对象,后者把数据值和行为特性融合在一起,类是现实世界的抽象的实体以编程形式出现.实例是这些对象的具体化.类是用来描述一类事物,类的对象指的 ...

  6. python学习day20 面向对象(二)类成员&成员修饰符

    1.成员 类成员 类变量 绑定方法 类方法 静态方法 属性 实例成员(对象) 实例变量 1.1实例变量 类实例化后的对象内部的变量 1.2类变量 类中的变量,写在类的下一级和方法同一级. 访问方法: ...

  7. Python:笔记(3)——面向对象编程

    Python:笔记(3)——面向对象编程 类和面向对象编程 1.类的创建 说明:和Java不同的是,我们不需要显示的说明类的字段属性,并且可以在后面动态的添加. 2.构造函数 构造函数的功能毋庸置疑, ...

  8. [python 译] 基于面向对象的分析和设计

    [python 译] 基于面向对象的分析和设计 // */ // ]]>   [python 译] 基于面向对象的分析和设计 Table of Contents 1 原文地址 2 引言 2.1 ...

  9. Python之面向对象与类

    本节内容 面向对象的概念 类的封装 类的继承 类的多态 静态方法.类方法 和 属性方法 类的特殊成员方法 子类属性查找顺序 一.面向对象的概念 1. "面向对象(OOP)"是什么? ...

随机推荐

  1. 原型和原型链 prototype和proto的区别

    原型 原型是function对象下的属性,它定义了构造函数的共同祖先,也就是一个父子级的关系,子对象会继承父对象的方法和属性 prototype是函数下的属性,对象想要查看原型使用隐式属性__Prot ...

  2. Git常用命令+本地连接远程仓库

    一.git命令整理 git config --global user.email "邮箱名":绑定GitHub邮箱 git config --global user.name &q ...

  3. .NET Worker Service 添加 Serilog 日志记录

    前面我们了解了 .NET Worker Service 的入门知识[1] 和 如何优雅退出 Worker Service [2],今天我们接着介绍一下如何为 Worker Service 添加 Ser ...

  4. GO汇编-函数

    GO汇编-函数 终于到函数了!因为Go汇编语言中,可以也建议通过Go语言来定义全局变量,那么剩下的也就是函数了.只有掌握了汇编函数的基本用法,才能真正算是Go汇编语言入门.本章将简单讨论Go汇编中函数 ...

  5. 中国摄像头CMOS需求潜力旺盛

    中国摄像头CMOS需求潜力旺盛 CMOS是Complementary Metal Oxide Semiconductor(互补金属氧化物半导体)的缩写.它是指制造大规模集成电路芯片用的一种技术或用这种 ...

  6. TensorRT 数据格式说明

    TensorRT数据格式说明 NVIDIA  TensorRT支持不同的数据格式.需要考虑两个方面:数据类型和布局. 数据类型格式 数据类型是每个单独值的表示.它的大小决定了数值的范围和表示的精度:分 ...

  7. 车联网V-2X智能汽车驾驶

    车联网V-2X智能汽车驾驶 早期的功能互联汽车无法满足全球车主针对不同应用和定制移动服务的各种需求.这导致较低的客户续订率,较高的建造和运营成本以及较低的组装率.通常,在没有统一平台的情况下,不同的车 ...

  8. HttpServer:一款Windows平台下基于IOCP模型的高并发轻量级web服务器

    HttpServer的特点1.完全采用IOCP模型,实现真正的异步IO,高并发.高可靠: 2.支持4G以上文件下载: 3.支持断点续传: 4.轻量级,体积小,服务器文件仅200多K,无任何依赖库: 5 ...

  9. Jmeter(五十一) - 从入门到精通高级篇 - jmeter之运动战(详解教程)

    1.简介 运动战是一种军事作战方式,依托较大的作战空间来换取时间移动兵力包围敌方,以优势兵力速战速决,运动战的运用归为这样一段话"避敌主力,诱敌深入,集中优势兵力逐个击破".今天宏 ...

  10. 绘制log()函数图像,并在图上标注选定的两个点

    绘制log()函数图像,并在图上标注选定的两个点 import math import matplotlib.pyplot as plt if __name__ == '__main__': x = ...