创建一个符合Python风格的对象(2)

创建一个符合Python风格的对象(1)中,定义了一个二维向量 Vector2d 类,现在以该类为基础,继续扩展,定义表示多维向量的Vector类。
支持的功能如下:

  • 基本的序列协议,__len____getitem__
  • 正确表述拥有很多元素的实例
  • 适当的切片支持,用于生产新的Vector实例
  • 综合各个元素的值计算散列值
  • 自定义的格式语言扩展

此外,通过 __getattr__ 方法实现属性的动态存取,以此取代 Vector2d 使用的只读特性——不过,序列类型通常不会这么做。

下面来一步步实现。

1.为了支持N维向量,让构造函数接受可迭代对象

    def __init__(self, components):
        # 把 Vector 的分量保存在一个数组中
        self._components = array(self.typecode, components)

2.为了支持迭代,使用self.components构建一个迭代器

    def __iter__(self):
        return iter(self._components)

3.使用reprlib.repr() 函数获取 self._components 的有限长度表示形式(如 array('d', [0.0, 1.0, 2.0, 3.0, 4.0, ...])

    def __repr__(self):
        components = reprlib.repr(self._components)
        # 去掉前面的 array('d' 和后面的 )。
        components = components[components.find('['):-1]
        return 'Vector({})'.format(components)

4.直接使用self.components构建bytes对象

    def __bytes__(self):
        return (bytes(ord([self.typecode])) + bytes(self._components))

5计算模

    def __abs__(self):
        """计算各分量的平方之和,然后再使用 sqrt 方法开平方"""
        return math.sqrt(sum(x * x for x in self))

6.针对frombytes,直接把 memoryview 传给构造方法,不用像前面那样使用 * 拆包

@classmethod
def frombytes(cls, octets):
    typecode = chr(octets[0])
    memv = memoryview(octets[1:]).cast(typecode)
    return cls(memv) 

7.为了支持序列协议,实现__len____getitem__方法

    def __len__(self):
        return len(self._components)

    def __getitem__(self, index):
        """自定义切片操作"""
        cls = type(self)
        #  如果 index 参数的值是 slice 对象,调用类的构造方法,使用 _components 数组的切片构建一个新 Vector 实例
        if isinstance(index, slice):
            return cls(self._components[index])
        # 如果 index 是 int 或其他整数类型,那就返回 _components 中相应的元素
        elif isinstance(index, numbers.Integral):
            return self._components[index]
        # 否则,抛出异常
        else:
            msg = '{.__name__} indices must be integers'
            raise TypeError(msg.format(cls))

8.动态存取属性
因为现在是N维向量,使用Vector2d中获取属性的方式显然太麻烦。
要想依旧使用my_obj.x方式获取属性,可以实现__getattr__方法,因为属性查找失败后,解释器会调用 __getattr__ 方法。

    # 定义几个可以获取的常用分量
    shortcut__names = 'xyzt'

    def __getattr__(self, name):
        """检查所查找的属性是不是 shortcut__names 中的某个字母,如果是,那么返回对应的分量。"""
        cls = type(self)
        # 如果属性名只有一个字母,可能是shortcut_names 中的一个
        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}'
        raise AttributeError(msg.format(cls, name))

但是仅仅实现这样一个方法还不够,需要注意到对于实例v,如果执行了v.x命令,实际上v对象就有x属性了,因此使用v.x不会调用__getattr__方法。
为了避免上述情况,需要改写Vector类中设置属性的逻辑,通过自定义__setattr__方法实现。

def __setattr__(self, name, value):
    cls = type(self)
    # 特别处理名称是单个字符的属性
    if len(name) == 1:
        # 如果 name 是 shortcut_names 中的一个,设置特殊的错误消息
        if name in cls.shortcut_names:
            error = 'readonly attribute {attr_name!r}'
        # 如果 name 是小写字母,为所有小写字母设置一个错误消息
        elif name.islower():
            error = "can't set attributes 'a' to 'z' in {cls_name!r}"
        # 否则,把错误消息设为空字符串
        else:
            error = ''
        #  如果有错误消息,抛出 AttributeError
        if error:
            msg = error.format(cls_name=cls.__name__, attr_name=name)
            raise AttributeError(msg)
    # 默认情况:在超类上调用 __setattr__ 方法,提供标准行为
    super().__setattr__(name, value)

在类中声明 __slots__ 属性也可以防止设置新实例属性。但是不建议只为了避免创建实例属性而使用 __slots__ 属性。__slots__ 属性只应该用于节省内存,而且仅当内存严重不足时才应该这么做。
另外,为了将该类实例变成是可散列的,需要保持Vector是不可变的。

9.支持散列和快速等值测试

    def __eq__(self, other):
        # 首先要检查两个操作数的长度是否相同,因为 zip 函数会在最短的那个操作数耗尽时停止,而且不发出警告。
        # 然后再依次比较两个序列中的每一个元素
        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)
        # 把 hashes 提供给 reduce 函数,使用 xor 函数计算聚合的散列值;第三个参数,0 是初始值
        return functools.reduce(operator.xor, hashes, 0)

10.格式化
Vector 类支持 N 个维度,所以这里使用球面坐标,格式后缀定义为'h'。这里的难点主要是涉及数学原理,理解意思即可。具体可以查看n 维球体

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]
        # 使用 itertools.chain 函数生成生成器表达式,无缝迭代向量的模和各个角坐标
        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))

下面给出完整代码

from array import array
import reprlib
import math
import numbers
import functools
import operator
import itertools


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 (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)

    def __abs__(self):
        return math.sqrt(sum(x * x for x in self))

    def __bool__(self):
        return bool(abs(self))

    def __len__(self):
        return len(self._components)

    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 = '{.__name__} indices must be integers'
            raise TypeError(msg.format(cls))

    shortcut_names = 'xyzt'

    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}'
        raise AttributeError(msg.format(cls, name))

    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())
            outer_fmt = '<{}>'
        else:
            coords = self
            outer_fmt = '({})'
        components = (format(c, fmt_spec) for c in coords)
        return outer_fmt.format(', '.join(components))

    @classmethod
    def frombytes(cls, octets):
        typecode = chr(octets[0])
        memv = memoryview(octets[1:]).cast(typecode)
        return cls(memv)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,099评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,473评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,229评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,570评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,427评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,335评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,737评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,392评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,693评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,730评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,512评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,349评论 3 314
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,750评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,017评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,290评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,706评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,904评论 2 335

推荐阅读更多精彩内容

  • 自己定义一个简单的二维欧几里得向量类型,使该类的行为跟真正的Python对象一样。该类所支持的主要特性如下 支持用...
    SHISHENGJIA阅读 529评论 0 2
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,511评论 18 139
  • 要点: 函数式编程:注意不是“函数编程”,多了一个“式” 模块:如何使用模块 面向对象编程:面向对象的概念、属性、...
    victorsungo阅读 1,447评论 0 6
  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,808评论 6 13
  • 定义类并创建实例 在Python中,类通过 class 关键字定义。以 Person 为例,定义一个Person类...
    绩重KF阅读 3,914评论 0 13