《流畅的Python》读书笔记——Python序列的修改、散列和切片

引言

信息检索领域经常使用 n 维向量,查询的文档和文本使用向
量表示,一个单词一个维度。这叫向量空间模型。在这个模型
中,一个关键的相关指标是余弦相关性(即查询向量与文档向量夹
角的余弦)。夹角越小,余弦值越趋近于 1,文档与查询的相关性
就越大。相关性越大也就是越相似,可用于分类、相似搜索以及推荐系统等。

本篇文章在上一篇的基础上,使用组合模式实现我们的n维向量。

Vector类v1:与Vector2d类兼容

Vector类的第一版要尽量与上一篇定义的Vector2d类兼容。

# -*- coding: utf-8 -*
#vector_v1.py
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) #通过iter函数构建一个迭代器

    def __repr__(self):
        components = reprlib.repr(self._components) #获取 self._components 的有限长度表示形式(如 array('d', [0.0, 1.0, 2.0, 3.0, 4.0,...]))
        components = components[components.find('['):-1] #去掉前面的array('d' 和后面的 )
        return 'Vector({})'.format(components)

    def __str__(self):
        return str(tuple(self))

    def __format__(self, format_spec = ''):
        components = (format(c,format_spec) for c in self) #使用内置的 format 函数把 fmt_spec 应用到向量的各个分量上
        return '({}, {})'.format(*components)

    def __bytes__(self):
        return (bytes([ord(self.typecode)]) + #把 typecode 转换成字节序列
        bytes(self._components)) # 用self._components构建bytes对象

    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  # 类方法使用classmethod装饰器修饰
    def frombytes(cls, octets):  # 通过cls传入类本身
        typecode = chr(octets[0])  # 从第一个字节中读取typecode
        memv = memoryview(octets[1:]).cast(typecode)  # 通过传入的字节序列创建一个memoryview,然后使用typecode转换
        return cls(memv)  # 直接把memoryview传递给构造函数

我们把构造函数修改为接收可迭代的对象为参数。

然后,我们在控制台访问它:

>>> from vector_v1 import Vector
>>> Vector([3.4,4.2])
Vector([3.4, 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, ...]) 

输出Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...]) 是因为用了reprlib生成概要形式。

协议和鸭子类型

“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子”。它不关注对象的类型,而是关注对象具有的行为(方法)。这里的意思是只要有下面两个方法,就是序列。

在Python中创建功能完善的序列类型无需使用基础,只需实现符合序列协议的方法。

Python的序列协议只需要__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._cards)

    def __getitem__(self, position):
        return self._cards[position]

该类实现了序列协议,能当成一个序列使用,即使它继承的是object

协议是非正式的,没有强制力。如果只是为了支持迭代,可以只实现__getitem__方法。

Vector类v2:可切片的序列

在v1中,我们通过_components数组来构造的向量。因此,支持序列协议非常简单:

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

def __getitem__(self, index):
    return self._components[index]

然后就能执行下述操作了:

>>> from vector_v1 import Vector
>>> v1 = Vector([3,4,5])
>>> len(v1)
3
>>> v1[0] #支持索引
3.0
>>> v1[0],v1[-1] 
(3.0, 5.0)
>>> v7 = Vector(range(7))
>>> v7[1:4] #支持切片
array('d', [1.0, 2.0, 3.0])

虽然支持切片,但是还不够完美,如果Vector实例的切片也会是Vector就好了。

为了把 Vector 实例的切片也变成 Vector 实例,我们不能简单地委托
给数组切片。我们要分析传给__getitem__方法的参数,做适当的处理。

切片原理

下来看个小例子先:

>>> class MySeq:
...     def __getitem__(self,index):
...             return index #直接返回索引
...
>>> s = MySeq()
>>> s[1]
1
>>> s[1:4] #变成了slice(1, 4, None)
slice(1, 4, None)
>>> s[1:4:2]
slice(1, 4, 2)
>>> s[1:4:2,9] #如果[]中有逗号,那么__getitem__收到的是元组
(slice(1, 4, 2), 9)
>>> s[1:4:2,7:9] #元组中有多个切片对象
(slice(1, 4, 2), slice(7, 9, None))

来看切片slice本身:

>>> slice #它是内置类型
<class 'slice'>
>>> dir(slice)
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'indices', 'start', 'step', 'stop']
>>>

结果中有个 indices 属性,这个方法有很大的作用:

>>> help(slice.indices)
Help on method_descriptor:

indices(...)
    S.indices(len) -> (start, stop, stride)
    
 给定长度为 len 的序列,计算 S 表示的扩展切片的起始
(start)和结尾(stop)索引,以及步幅(stride)。超出边界的
索引会被截掉,这与常规切片的处理方式一样。

换句话说,indices方法用于优雅地处理缺失索引和负数索引,以及长度超过目标序列的切片:

假设有个长度为5的序列,例如’ABCDE’:

>>> slice(None,10,2).indices(5) #'ABCDE'[:10:2] 等同于 'ABCDE'[0:5:2]
(0, 5, 2)
>>> slice(-3,None,None).indices(5) #'ABCDE'[-3:]等同于'ABCDE'[2:5:1]
(2, 5, 1)

能处理切片的__getitem__方法

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

def __getitem__(self, index):
   cls = type(self)  # 获取类信息
   if isinstance(index, slice): #如果是切片:
       return cls(self._components[index]) # ,使用 _components 数组的切片构建一个新 Vector 实例。
   elif isinstance(index,numbers.Integral): # 如果是int或其他整数类型
       return self._components[index] # 索引直接返回值
   else:
       msg = '{cls.__name__} indices must be integers'
       raise TypeError(msg.format(cls=cls)) #抛出异常

下面测试我们新改造的类:

>>> from vector_v1 import Vector
>>> v7 = Vector(range(7))
>>> v7[-1]
6.0
>>> v7[1:4]
Vector([1.0, 2.0, 3.0])
>>> v7[-1:]
Vector([6.0])
>>> v7[1,2]
Traceback (most recent call last):
	...
    raise TypeError(msg.format(cls=cls)) #抛出异常
TypeError: Vector indices must be integers

Vector类v3:动态存取属性

假设我们想通过单个字母访问前几个分量,如用x,y和z替代v[0],v[1],v[2]

可以通过特殊方法__getattr__来实现:

    shortcut_names = 'xyzt' #先定义一些字母
 
    def __getattr__(self, name):
        cls = type(self)
        if len(name) == 1:
            pos = cls.shortcut_names.find(name) #如果属性名只有一个字母,可能是shortcut_names中的一个
            if 0 <= pos < len(self._components):
                return self._components[pos]
        msg = '{.__name__!r} object has no attribute {!r}'
        raise AttributeError(msg.format(cls, name))

在属性查找失败后,解释器会调用 __getattr__方法。

>>> from vector_v1 import Vector #我全部放到这个文件中,所以都是v1
>>> v = Vector(range(5))
>>> v
Vector([0.0, 1.0, 2.0, 3.0, 4.0])
>>> v.x #使用x获取第一个元素
0.0
>>> v.x = 10 #为v.x赋值,应该抛出异常
>>> v.x #读取得到的是新值
10
>>> v
Vector([0.0, 1.0, 2.0, 3.0, 4.0]) #但是内部还是原来的值

仅当对象没有指定名称的属性时,Python 才会调用__getattr__方法,这是一种后备机制。可是,像 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.islow(): # 如果是小写字母
            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__ 方法,以防对象的行为不一致。

Vector类v4:散列和快速等值测试

import functools
import operator
...

def __eq__(self, other): #eq方法已经实现过
    return tuple(self) == tuple(other)

def __hash__(self):
    hashes = (hash(x) for x in self._components)
    # 可以替换成 hashes = map(hash,self._components)
    return functools.reduce(operator.xor,hashes,0) #0作为初始值

这里的hash是通过对每个元素求hash值,并进行异或操作:hash(v[0]) ^ hash(v[1]) ^ hash(v[2]) ...

接着,我们优化下eq函数,上面的eq函数,如果遇到了n维(n很大)向量时,效率十分低下。

def __eq__(self, other):
    if len(self) != len(other): #长度都不相等的话就没必要比较了
        return False
    for a,b in zip(self,other) :
        if a != b:
            return False #只要有两个分量不同,则提前返回False
    return  True

zip 函数生成一个由元组构成的生成器,元组中的元素来自参数传
入的各个可迭代对象。

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

zip函数的用例:

>>> zip(range(3),'ABC')
<zip object at 0x00000124C88FDA48>
>>> list(zip(range(3),'ABC')) #zip返回的是元组,这里用list包装起来了
[(0, 'A'), (1, 'B'), (2, 'C')]
>>> list(zip(range(3),'ABC',[0.0,1.1,2.2,3.3])) #可以处理任意多个,最后一个列表有4个元素,但是前两个参数只有三个元素,迭代完前面的就会停止
[(0, 'A', 0.0), (1, 'B', 1.1), (2, 'C', 2.2)]
>>> from itertools import zip_longest #使用最长的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)]
发布了131 篇原创文章 · 获赞 38 · 访问量 12万+

猜你喜欢

转载自blog.csdn.net/yjw123456/article/details/99692237