<jupyter_start><jupyter_text>序列的修改、散列和切片本章在上一章的基础上，为自定义的向量类赋予更多序列相关的特性：* 支持基本的序列协议* 正确表达拥有大量元素的序列类型实例* 对切片的支持* 扩展格式化输出方式  自定义序列类型为了扩展上一章中的二维向量到n维向量，本章对上一章中自定义向量类的构造方法进行了改进，以支持任意维度向量对象的构建。为了方便实现，本章遵循内置序列类型的做法 —— 接收一个可迭代对象为参数，并在这个可迭代对象的基础上创建自定义向量对象。 基础任意维度向量类下述简单描述了一个基础任意维度向量类应当具有的特性，其他特性将逐步扩展。```Pythonclass BasicVector:    typecode = "d"    def __init__(self, components):        self._components = array(self.typecode, components)        def __iter__(self):         支持迭代        pass    def __repr__(self):         支持repr()        pass    def __str__(self):         支持print()        pass            def __bytes__(self):         支持转化为字节        pass    def __eq__(self, other):         等值测试        pass    def __abs__(self):         向量模长        pass    def __bool__(self):         支持bool()        pass    @classmethod    def frombytes(cls, octets):         支持从字节中重新生成实例        pass```具体实现如下<jupyter_code>from array import array
import reprlib
import math

class Vector:
    typecode = "d"

    def __init__(self, components: iter):
        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)<jupyter_output><empty_output><jupyter_text>序列以及可切片的序列在Python中只要实现\_\_len\_\_()方法以及\_\_getitem\_\_()方法就能实现一个基本的序列类型。这一特性是Python的序列协议支持的，这一协议仅在文档中定义，而不在代码中定义。这一特性也体现了Python中鸭子类型的作用，即无需使用继承使得自定义类被识别为某一特定类别的类，而仅需要实现相应协议要求的方法 —— 我们说它是序列，因为它的行为像序列。 可切片序列若将\_\_len\_\_()以及\_\_getitem\_\_()方法的具体实现委托给对象中的序列类型属性则能非常快速的实现切片操作以及其他序列操作。例如对于上述定义的自定义向量类，若将\_\_len\_\_()和\_\_getitem\_\_()方法的具体实现委托给_components数组则可以非常快速的实现切片操作，具体代码如下：```Pythondef __len__(self):    return len(self._components)def __getitem__(self, index):    return self._components[index]```上述实现方法虽然实现了序列协议，但是有一个致命缺陷 —— 对于切片来说，我们希望对自定义向量类进行切片后，其返回的对象也应当是向量类，但是上述实现方法将会返回一个数组。因此上述的\_\_getitem\_\_()方法可以进行如下改写：```Pythondef __getitem__(self, index):    cls = type(self)    if isinstance(index, slice):        return cls(self._components[index])    elif isinstance(index, numbers.Integral):        return cls([self._components[index]])    else:        msg = "{cls.__name__} indices must be integers"        raise TypeError(msg.format(cls=cls))```上述实现实际上参考了Python内置类型的行为，包括上述的逻辑判断以及报错信息。这一操作是为了模仿Python内置的类型。但是实际使用中完全可以使用更适用于当前项目的实现方法 —— 例如支持多维索引，这一特性正是Numpy array支持的特性。关于多维索引，实际上，Python已经提供了很好的基础。对于传入\_\_getitem\_\_()方法的index参数，若index参数包含多个以逗号隔开的切片索引，Python会自动使用slice对象解析这些索引并按照顺序存储到一个元组中。Python内置的序列类型以及上述实现的向量类均不支持多维索引，因此不会对这一包含多个slice对象的元组进行解析，并且直接raise TypeError；而Numpy array则支持多维索引，具体表现为尝试对这一包含多个slice对象的元组进行解析。<jupyter_code>import numpy as np

print("numpy array 多维索引")
print("当维度符合要求时")
np_array = np.arange(0, 20, 1).reshape(2, 10)
print("切片结果：\n", np_array[0:2, 1:3])
print("当切片维度大于array维度时， raise IndexError")
np_array_2 = np.arange(0, 20, 1)
print("切片结果：\n", np_array_2[0:2, 1:3])<jupyter_output>numpy array 多维索引
当维度符合要求时
切片结果：
 [[ 1  2]
 [11 12]]
当切片维度大于array维度时<jupyter_text>\_\_getattr\_\_\_\_getattr\_\_方法提供了在属性查找失败后的备选处理方案，通过实现\_\_getattr\_\_方法能够自定义属性查找失败后的行为。相较于上一章的向量类，由于本章定义的向量类允许n维向量，因此删去了类中的x, y, z属性。若仍期望使用x，y，z，t访问向量的前4个元素，一种实现方法是：在构造方法中定义这四个变量，但是这一处理略显臃肿；另一种实现方法是：并不显式的定义x，y，z，t属性，而通过\_\_getattr\_\_方法动态处理对前4个元素的访问请求。```Pythonshortcut_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))```上述实现方法会动态查找并返回x，y，z，t对应的值。值得注意的是，\_\_getattr\_\_仅能处理读取，而不能处理赋值。在上述实现的基础上，若尝试对x，y，z，t赋值则会创建新的实例属性 —— 这一行为会显得很奇怪，当再次访问被赋值的属性时会传回被赋予的值，但是当获取整个序列时，则会发现序列中对应位置的值没有变化。为了避免上述奇怪的行为，可以实现\_\_setattr\_\_()方法，当尝试对x，y，z，t赋值时直接raise AttributeError  散列以及快速等值测试 散列上一章中的\_\_hash\_\_方法由各分量的hash值进行按位异或运算获得，对于扩展到n维的分量仅读向量类，也可以按照相似的思路执行。具体来说，执行如下两步：* 计算每一个分量的hash值* 对这些hash值进行按位异或运算上述两步实际上是一种映射归纳计算 —— 映射：从分量到hash值，归纳：计算这些hash值的聚合结果```Pythondef __hash__(self):    hashes = map(hash, self._components)    return functools.reduce(operator.xor, hashes)``` 等值测试上一章的\_\_eq\_\_方法将等值测试委托给了tuple，对于仅有两维的向量类来说，这一操作没有问题；但是对于n维的向量类，这一方法比较低效（需要构建两个元组），更为有效的方法是直接比较每一个分量```Pythondef __eq__(self, other):    return len(self) == len(other) and all(a==b for a, b in zip(self, other))```  扩展格式化输出主要是扩展极坐标的输出方式，对于n维向量来说，希望返回其对应的超球坐标。其实现如下：```Pythondef 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))``` 综合上述功能扩展可以得到如下的自定义n维向量类<jupyter_code>from array import array
import functools
import operator
import reprlib
import math
import numbers
import itertools

class Vector:
    typecode = "d"

    def __init__(self, components: iter):
        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 = map(hash, self._components)
        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 = "{cls.__name__} indices must be integers"
            raise TypeError(msg.format(cls=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 __setattr__(self, name, value):
        cls = type(self)
        if len(name) == 1:
            if name in cls.shortcut_names:
                error = "readonly attribute {attr_name!r}"
            elif name.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=name)
                raise AttributeError(msg)
        
        super().__setattr__(name, value)
    
    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)
# some examples
test_v = Vector(range(7))
print("print()函数: ", test_v)
print("repr()函数: ", repr(test_v))
print("切片: ", test_v[:3])
print("动态属性访问: ", test_v.x)
print("超球坐标格式化输出: ", format(test_v, ".2h"))<jupyter_output>print()函数:  (0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0)
repr()函数:  Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...])
切片:  (0.0, 1.0, 2.0)
动态属性访问:  0.0
超球坐标格式化输出:  <9.5, 1.6, 1.5, 1.4, 1.2, 1.1, 0.88>
{'fraction_non_alphanumeric': 0.13350340136054423, 'fraction_numerical': 0.011373299319727892, 'mean_word_length': 3.1985720660419457, 'pattern_counts': {'":': 0, '<': 19, '<?xml version=': 0, '>': 13, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '3', 'dir': 'jupyter-structured-clean-dedup', 'id': '415459', 'license': 'MIT', 'n_tokens_mistral': 4307, 'n_tokens_neox': 4580, 'n_words': 606, 'path': '10_SequenceHackingHashingAndSlicing/10_SequenceHackingHashingAndSlicing.ipynb', 'repo_name': 'koolo233/FluentPythonNotes', 'size': '9408'}