• 流畅的python——10 序列的修改、散列和切片


    十、序列的修改、散列和切片

    不要检查它是不是鸭子、它的叫声像不像鸭子、它的走路姿势像不像鸭子,等等。具体检查什么取决于你想使用语言的哪些行为。(comp.lang.python,2000 年 7月 26 日) ——Alex Martelli

    多维向量

    In [20]: from array import array
    
    In [21]: import reprlib
    
    In [22]: import math
    
    In [29]: 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)
        ...:     def __len__(self):
        ...:         return len(self._components)
        ...:     def __getitem__(self, index):  # 支持 [] 和 切片
        ...:         return self._components[index]
    

    我使用 reprlib.repr 的方式需要做些说明。这个函数用于生成大型结构或递归结构的安全表示形式,它会限制输出字符串的长度,用 '...' 表示截断的部分。我希望 Vector实例的表示形式是 Vector([3.0, 4.0, 5.0]) 这样,而不是 Vector(array('d',[3.0, 4.0, 5.0])),因为 Vector 实例中的数组是实现细节。

    编写 __repr__ 方法时,本可以使用这个表达式生成简化的 components 显示形式:reprlib.repr(list(self._components))。然而,这么做有点浪费,因为要把self._components 中的每个元素复制到一个列表中,然后使用列表的表示形式。而是直接把 self._components 传给 reprlib.repr 函数,然后去掉 [] 外面的字符。

    调用 repr() 函数的目的是调试,因此绝对不能抛出异常。如果 __repr__ 方法的实现有问题,那么必须处理,尽量输出有用的内容,让用户能够识别目标对象。

    协议和鸭子类型

    在面向对象编程中,协议是非正式的接口,只有在文档中定义,在代码中不定义。任何类只要使用标准的签名和语义实现了协议的方法,就能用在任何期待实现协议功能的地方。其是不是哪个类的子类无关紧要,只要提供了所需的方法即可。

    比如:序列协议:只要实现 __len____getitem__ 两个方法。

    我们说它是序列,因为它的行为像序列,这才是重点。

    协议是非正式的,没有强制力,因此如果你知道类的具体使用场景,通常只需要实现一个协议的部分。例如,为了支持迭代,只需要实现 __getitem__ 方法,没必要实现 __len__ 方法。

    支持切片

    如果得到的 Vector 对象的切片是一个列表,会丢失很多功能,如果得到是还是一个 vector 对象,就很好了。

    切片原理

    In [1]: class Y:
       ...:     def __getitem__(self,index):
       ...:         return index
       ...:
    
    In [2]: y = Y()
    
    In [3]: y[1]
    Out[3]: 1
    
    In [4]: y[1:2]
    Out[4]: slice(1, 2, None)
    
    In [5]: y[1:2:2,9]  # 逗号隔开,index 是元组
    Out[5]: (slice(1, 2, 2), 9)
    
    In [6]: y[1:2,2:3]  # 支持多个切片
    Out[6]: (slice(1, 2, None), slice(2, 3, None))
    

    indices 方法

    indices 方法开放了内置序列实现的棘手逻辑,用于优雅地处理缺失索引和负数索引,以及长度超过目标序列的切片。这个方法会“整顿”元组,把 start、stop 和 stride 都变成非负数,而且都落在指定长度序列的边界内。

    In [11]: slice(None,11,2).indices(5)
    Out[11]: (0, 5, 2)
    
    In [12]: slice(None,None,None).indices(10)
    Out[12]: (0, 10, 1)
    

    dir() 和 help(),发现 slice.indices() 方法。这也表明交互式控制台是个有价值的工具,能发现新事物。

    In [10]: help(slice.indices)
    Help on method_descriptor:
    
    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.
    

    __getitem__ 处理切片

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

    __getattr__ : obj.x

    前四个位置的元素分别为:x, y, z, t

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

    测试:

    >>> v = Vector(range(5))
    >>> v
    Vector([0.0, 1.0, 2.0, 3.0, 4.0])
    >>> v.x
    0.0
    >>> v.x = 10  # 赋值给 v 新的 x 属性
    >>> v.x  # 优先读取 x 属性,而不是直接调用 __getattr__
    10
    >>> v
    Vector([0.0, 1.0, 2.0, 3.0, 4.0])
    

    仅当对象没有指定名称的属性时,Python 才会调用那个方法,这是一种后备机制。可是,像 v.x = 10 这样赋值之后,v 对象有 x 属性了,因此使用 v.x 获取 x 属性的值时不会调用 __getattr__ 方法了,解释器直接返回绑定到 v.x 上的值,即 10。另一方面,__getattr__ 方法的实现没有考虑到 self._components 之外的实例属性,而是从这个属性中获取 shortcut_names 中所列的“虚拟属性”。

    实现 __setattr__ 方法

    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():  # 如果 name 是小写字母,为所有小写字母设置一个错误消息。
                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)  # 默认情况:在超类上调用 __setattr__ 方法,提供标准行为。
    

    为了给 AttributeError 选择错误消息,查看了内置的 complex 类型的行为,因为 complex 对象是不可变的

    注意,我们没有禁止为全部属性赋值,只是禁止为单个小写字母属性赋值,以防与只读属性 x、y、z 和 t 混淆。

    多数时候,如果实现了 __getattr__ 方法,那么也要定义 __setattr__ 方法,以防对象的行为不一致。

    __setitem__ 方法,支持 v[0] = 1.1

    可散列对象

    累积计算

    operator 模块以函数的形式提供了 Python 的全部中缀运算符,从而减少使用 lambda 表达式。

    >>> n = 0
    >>> for i in range(1, 6):
    ...     n ^= i
    >>> import functools
    >>> functools.reduce(lambda a, b: a^b, range(6))
    >>> import operator
    >>> functools.reduce(operator.xor, range(6))
    

    使用 reduce 函数时最好提供第三个参数,reduce(function, iterable, initializer),这样能避免这个异常:TypeError: reduce() of empty sequence with no initial value(这个错误消息很棒,说明了问题,还提供了解决方法)。如果序列为空,initializer 是返回的结果;否则,在归约中使用它作为第一个参数,因此应该使用恒等值。比如,对 +、| 和 ^ 来说, initializer 应该是 0;而对 * 和 & 来说,应该是 1。

    from array import array
    import reprlib
    import math
    import functools 
    import operator
    class Vector:
        typecode = 'd'
        def __eq__(self, other):
            return tuple(self) == tuple(other)
        def __hash__(self):
            hashes = (hash(x) for x in self._components)
            return functools.reduce(operator.xor, hashes, 0)
    

    映射过程计算各个分量的散列值,归约过程则使用 xor 运算符聚合所有散列值。把生成器表达式替换成 map 方法,映射过程更明显:

    def __hash__(self):
        hashes = map(hash, self._components)
        return functools.reduce(operator.xor, hashes)
    

    在 Python 2 中使用 map 函数效率低些,因为 map 函数要使用结果构建一个列表。但是在 Python 3 中,map 函数是惰性的,它会创建一个生成器,按需产出结果,因此能节省内存——这与示例 10-12 中使用生成器表达式定义 __hash__ 方法的原理一样。

    改进 __eq__ 方法

    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
    

    前面比较长度的测试是有必要的,因为一旦有一个输入耗尽,zip 函数会立即停止生成值,而且不发出警告。

    改进:上个改进效率很好,不过用于计算聚合值的整个 for 循环可以替换成一行 all 函数调用:如果所有分量对的比较结果都是 True,那么结果就是 True。只要有一次比较的结果是 False,all 函数就返回 False。zip 也是返回生成器。

    def __eq__(self, other):
        return len(self) == len(other) and all(a == b for a, b in zip(self, other))
    

    出色的 zip 函数

    使用 for 循环迭代元素不用处理索引变量,还能避免很多缺陷,但是需要一些特殊的实用函数协助。其中一个是内置的 zip 函数。使用 zip 函数能轻松地并行迭代两个或更多可迭代对象,它返回的元组可以拆包成变量,分别对应各个并行输入中的一个元素。

    >>> from itertools import zip_longest  # 这个模块可以填充缺失值
    >>> list(zip_longest(range(3), 'ABC', [0.0, 1.1, 2.2, 3.3], fillvalue=-1))
    [(0, 'A', 0.0), (1, 'B', 1.1), (2, 'C', 2.2), (-1, -1, 3.3)]
    

    输出格式化:__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'): # 超球面坐标
            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))
    

    协议,是鸭子类型语言使用的非正式接口。

    我们经常分析 Python 标准对象的行为,然后进行模仿,让 Vector 的行为符合 Python 风格。

    模仿内置类型实现类时,记住一点:模仿的程度对建模的对象来说合理即可。例如,有些序列可能只需要获取单个元素,而不必提取切片。 ——Python 语言参考手册中“Data Model”一章

    不要为了满足过度设计的接口契约和让编译器开心,而去实现不需要的方法,我们要遵守 KISS 原则(http://en.wikipedia.org/wiki/KISS_principle)。

    “地道”并不是指使用最鲜为人知 的语言特性。

    python 风格的求和方式

    我喜欢 Evan Simpson 的代码,不过也喜欢 David Eppstein 对此给出的评论:

    如果你想计算列表中各个元素的和,写出的代码应该看起来像是在“计算元素之和”,而不是“迭代元素,维护一个变量 t,再执行一系列求和操作”。如果不能站在一定高度上表明意图,让语言去关注低层操作,那么要高级语言干嘛?

    随后,Alex 建议提供并实现了 sum() 函数。这次讨论之后三个月,Python 2.3 就内置了这个函数。因此,Alex 喜欢的句法变成了标准:

    >>> sum([sub[1] for sub in my_list])
    60
    

    下一年年末(2004 年 11 月),Python 2.4 发布了,这一版引入了生成器表达式。因此,在我看来,Guy Middleton 那个问题目前最符合 Python 风格的答案是:

    >>> sum(sub[1] for sub in my_list)
    60
    
  • 相关阅读:
    深入浅出进程与线程的基本概念
    python中with的用法
    浮点型数据在内存中存储的表示
    自问问题列表以及网络答案整理
    看java源代码
    【设计模式】工厂方法
    SQL实现递归及存储过程中 In() 参数传递解决方案
    app与server联系
    添加service到SystemService硬件服务
    noproguard.classes-with-local.dex
  • 原文地址:https://www.cnblogs.com/pythonwl/p/15344604.html
Copyright © 2020-2023  润新知