python序列的修改散列和切片
Posted Sakura
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了python序列的修改散列和切片相关的知识,希望对你有一定的参考价值。
新Vector类
接原vector类定义的新Vector类,原向量类是二维,现定义多维向量类:
from array import array import reprlib import math class Vector: typecode = \'d\' shortcut_names = \'xyzt\' 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)
协议和鸭子类型
协议:
1.协议是非正式的接口,没有强制力;协议只在文档中定义,在代码中不定义。
2.python有很多协议,如可调用对象协议,哈希协议,序列类协议,容器类协议等等等等。
3.如果知道类的具体使用场景,通常只需要实现协议的一个部分,例如为了支持迭代,只需要实现__getitem__方法,而不必要提供__len__方法。
4.需要成为相对应的鸭子类型,那就实现相关的协议,即相关的__method__。例如实现序列协议(__len__和__getitem__),这个类就表现得像序列。
鸭子类型:
1.(DuckTyping)鸭子类型在动态语言中用的较多,是动态类型语言设计的一种风格。在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口决定,而是由当前方法和属性的集合决定。通俗来说就是并不关心对象是什么类型,只关心行为。
2.当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子;即它的行为是鸭子的行为,那么可以认为它是鸭子。
3.在Python文档中,如果看到“文件类对象“(表现得像文件的对象),通常说的就是协议,这个对象就是鸭子类型。这是一种简短的说法,意思是:“行为基本与文件一致,实现了部分文件接口,满足上下文相关需求的东西。”
FrenchDeck实现了序列协议,即实现了__len__和__getitem__,那么它就可以看作是序列:
import collections Card = collections.namedtuple(\'Card\', [\'rank\',\'suit\']) class FrenchDeck: ranks = [str(n) for n in range(2, 11)] + list(\'JQKA\') suits = \'spades diamonds clubs hearts\'.split() def __init__(self): self._cards = [Card(rank, suit) for suit in self.suits for rank in self.ranks] def __len__(self): return len(self._cars) def __getitem__(self, position): return self._cards[position]
切片原理
class Myseq: def __getitem__(self, index): return index #返回索引 s = Myseq() print(s[1]) print(s[1:4]) print(s[1:4:2]) print(s[1:4:2, 7]) print(s[1:4:2, 7:9]) #结果 1 #单个索引 slice(1, 4, None) #返回slice对象, slice(1, 4, 2) (slice(1, 4, 2), 7) (slice(1, 4, 2), slice(7, 9, None)) #返回slice对象元组
审查slice对象:
print(slice) print(dir(slice)) <class \'slice\'> [\'__class__\', \'__delattr__\', \'__dir__\', \'__doc__\', \'__eq__\', \'__format__\', \'__ge__\', \'__getattribute__\', \'__gt__\', \'__hash__\', \'__init__\', \'__le__\', \'__lt__\', \'__ne__\', \'__new__\', \'__reduce__\', \'__reduce_ex__\', \'__repr__\', \'__setattr__\', \'__sizeof__\', \'__str__\', \'__subclasshook__\', \'indices\', \'start\', \'step\', \'stop\']
slice是内置类型,它有\'indices\'方法, \'start\', \'step\', \'stop\'数据属性
indices(...) S.indices(len) -> (start, stop, stride) Assuming a sequence of length len, calculate the start and stop indices, and the stride length of the extended slice described by S. Out of bounds indices are clipped in a manner consistent with the handling of normal slices.
给定长度为len的序列,计算S表示的扩展切片的起始和结尾索引,以及步幅。超出边界的索引会被截掉,这与常规切片处理方式一样。
假如给定一个序列长度为5:
print(slice(None, 10, 2).indices(5)) print(slice(-3, None, None).indices(5)) (0, 5, 2) #切片[None, 10, 2]将转换为[0, 5, 2],因为序列长度最多为5 (2, 5, 1) #切片[-3, None, None]将转换为[2, 5, 1],-3位置即序列的2位置,长度为5,跨度默认为1
让Vector类能正常处理切片
为了让Vector类能正确处理切片,更改__getitem__方法。
def __getitem__(self, item): cls = type(self) if isinstance(item, slice): return cls(self._components[item]) #参数是slice对象,返回 elif isinstance(item, numbers.Integral): return self._components[item] #参数单个元素,返回单个元素 else: msg = \'{cls.__name__} indices must be integers\' raise TypeError(msg.format(cls=cls)) #抛出异常
动态存取属性
属性查找失败时解释器会调用__getattr__方法。即:对于obj.x表达式,python会检查obj实例有没有名为x的属性;如果没有,到类(obj.class)中查找,如果还没有,顺着继承树继续查找,如果依旧找不到,调用obj所属类中定义的__getattr__方法,传入self和属性名称的字符串形式(如‘x’)
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)) if __name__ == \'__main__\': V7 = Vector(range(7)) print(V7) print(V7.y) V7.y = 10.0 print(V7.y) print(V7)
测试结果:
(0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0) 1.0 10.0 #第二项为10 (0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0) #第二项为1,矛盾
造成这种结果的原因是__getattr__的运作方式:仅当对象没有指定名称的属性时,python才会调用那个方法,这是一种后备机制。可是运行过V7.y = 10之后V7就有y属性了,因此使用V7.y属性值时不会调用__getattr__方法,解释器直接返回绑定值,即10。另一方面,__getattr__方法的实现没有考虑到self._components之外的实例属性,而是从这个属性中获取shortcut_names中所列的虚拟属性。
修改在Vector类中设置属性的逻辑以应对前后矛盾:
def __setattr__(self, name, value): cls = type(self) if len(name) == 1: if name in cls.shortcut_names: error = \'readonly attribute {attr_name!r}\' #xyzt只读 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) #委托给超类的方法
多数时候,如果实现了__getattr__方法,那么也要实现__setattr__方法,以防出现示例那样对象行为不一致。如果想允许修改分量,可以使用__setitem__方法或实现__setattr__方法。
为Vector类实现散列协议
def __hash__(self): #法一,for循环迭代 hashes = (hash(x) for x in self._components) n = 0 for i in hashes: n ^= i return n def __hash__(self): #法二lambda表达式 hashes = (hash(x) for x in self._components) return functools.reduce(lambda a, b: a ^ b, hashes, 0) def __hash__(self): #库函数,推荐 hashes = (hash(x) for x in self._components) return functools.reduce(operator.xor, hashes, 0)
reduce()函数第三个参数是初始值,防止序列为空的异常。一般 +,|,^来说用0;而*和&用1。
或者使用隐射规约计算:
def __hash__(self): hashes = map(hash, self._components) return functools.reduce(operator.xor, hashes, 0)
相等性判断优化
对于有上千万个分量的Vector实例来说,相等性的比较效率十分低下,因为要复制两个操作数创建两个元组。可修改为:
def __eq__(self, other): if len(self) != len(other): return False for a, b in zip(self, other): if a != b: return False return True
或者:
def __eq__(self, other): return len(self) == len(other) and all(a == b for a, b in zip(self, other))
zip函数生成一个由元组构成的生成器,元组中各个元素来自参数传入的各个可迭代对象。它返回的元组可以拆包成变量,分别对应于各个并行输入中的一个元素。示例:
from itertools import zip_longest list1 = list(zip(range(3), \'ABC\')) print(list1) list2 = list(zip(range(3), \'ABC\', [0.1, 1.2, 2.3, 3.4])) print(list2) list3 = list(zip_longest(range(3), \'ABC\', [0.1, 1.2, 2.3, 3.4], fillvalue=-1)) print(list3) #结果 [(0, \'A\'), (1, \'B\'), (2, \'C\')] [(0, \'A\', 0.1), (1, \'B\', 1.2), (2, \'C\', 2.3)] [(0, \'A\', 0.1), (1, \'B\', 1.2), (2, \'C\', 2.3), (-1, -1, 3.4)]
1.当一个可迭代对象耗尽后,它不发出警告就停止
2.zip_longest有所不同,fillvalue作为填充(默认是None),直到最长的可迭代对象耗尽
以上来自《流畅的python》
以上是关于python序列的修改散列和切片的主要内容,如果未能解决你的问题,请参考以下文章
Fluent_Python_Part4面向对象,10-seq-hacking,序列的修改散列和切片