Python 使用列表、字典和set

Python 使用列表、字典和set,Python的序列对象(例如列表)是可迭代的,当然还有其他一些特点,可以把它们看作实例化的可迭代对象。在前面的例子中,使用了tuple()函数将生成器表达式或生成器函数的输出收集到单个元组对象中。也可以将序列实例化,以生成列表对象。

Python的列表推导提供了实例化生成器的一种简单方法:添加方括号[]。生成器表达式和列表推导基本没有区别,不用纠结于生成器表达式和使用了生成器表达式的列表推导的区别。

相关示例如下:

>>> range(10)
range(0, 10)
>>> [range(10)]
[range(0, 10)]
>>> [x for x in range(10)]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> list(range(10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

第一个例子是range对象,是一种生成器函数。它是惰性的,所以创建后不生成任何数值。

 range(10)是惰性的,仅在需要迭代所有值时才会执行10次求值。

第二个例子是一个包含生成器函数的列表,[]语法创建了一个包含range对象的列表字面量,不对迭代器生成的值执行任何求值操作。

第三个例子演示了如何通过把生成器函数包含在生成器表达式中来创建列表推导。生成器表达式x for x in range(10)对生成器函数range(10)求值,返回结果保存在列表对象里。

另外,还可以使用list()函数,基于可迭代对象或者生成器表达式创建列表。该方法同样适用于set()tuple()dict()

 list(range(10))函数会对内部的生成器函数求值,而[range(10)]列表字面量不会。

在Python中,可以通过[]{}这样的简单形式创建列表、字典和set,而要实例化元组对象,必须使用tuple()函数。为了保持代码风格一致,建议统一使用函数名,如list()tuple()set()实例化对象。

前面数据清洗的例子使用一个复合函数生成了一个包含4个元组的列表,该函数如下所示:

with open("Anscombe.txt") as source:
    data = head_split_fixed(row_iter(source))
    print(list(data))

将复合函数的返回结果赋给变量datadata如下所示:

[['10.0', '8.04', '10.0', '9.14', '10.0', '7.46', '8.0', '6.58'],
 ['8.0', '6.95', '8.0', '8.14', '8.0', '6.77', '8.0', '5.76'],
 ...
 ['5.0', '5.68', '5.0', '4.74', '5.0', '5.73', '8.0', '6.89']]

至此还需要些许处理才能使之达到可用状态。首先要从八元组中拆出4组数据,下面的函数实现了按列分组:

from typing import Tuple, cast

Pair = Tuple[str, str]
def series(
        n: int, row_iter: Iterable[List[Text]]
    ) -> Iterator[Pair]:
    for row in row_iter:
        yield cast(Pair, tuple(row[n * 2: n * 2 + 2]))

这个函数将相邻的两列分成一组,按照从0到3的顺序一共分成4组,用组内的两列生成一个tuple对象。其中cast()函数用于显式说明mypy工具返回的结果是一个元素为字符串的二元组。之所以要特别指明,是因为mypy等工具很难分析出tuple(row[n * 2: n * 2 + 2])从行集合中取出的是两个元素。

使用如下函数可以生成嵌套元组的集合:

with open("Anscombe.txt") as source:
    data = tuple(head_split_fixed(row_iter(source)))
    sample_I = tuple(series(0, data))
    sample_II = tuple(series(1, data))
    sample_III = tuple(series(2, data))
    sample_IV = tuple(series(3, data))

这里用tuple()函数处理之前由head_split_fixed()row_iter()方法组成的复合函数的返回值,生成的对象可供后续函数复用。如果这里没有把返回值实例化为一个元组,后面的处理函数只有第一个能获得实际输出数据,之后作为数据源的迭代器被置空,再访问它只会返回空结果。

series()将提取出的两个数据转换为Pair对象,然后用tuple()函数实例化返回的嵌套元组序列,以便进一步处理各元素。

sample_I中的数据如下所示:

(('10.0', '8.04'), ('8.0', '6.95'), ('13.0', '7.58'),
('9.0', '8.81'), ('11.0', '8.33'), ('14.0', '9.96'),
('6.0', '7.24'), ('4.0', '4.26'), ('12.0', '10.84'),
('7.0', '4.82'), ('5.0', '5.68'))

另外3个序列与上面的序列虽然值不同,但结构相同。

整个处理流程的最后一步是把得到的字符串转换为数值,然后计算各项统计数据。这一步中的转换通过float()函数实现,该函数可以放在许多不同的地方。

如下所示使用float()函数:

mean = (
    sum(float(pair[1]) for pair in sample_I) / len(sample_I)
)

这样就算出了每个二元组中y的平均值。可以用下面的方法算出各个集合的统计值:

for subset in sample_I, sample_II, sample_III, sample_III:
    mean = (
        sum(float(pair[1]) for pair in subset) / len(subset)
    )
    print(mean)

至此就计算出了源数据库中每对数据的y的平均值,并且创建了一个以命名元组为元素的元组序列,用于描述源数据库中的数据。pair[1]这种写法可读性较差。

为了降低内存占用、提高性能,建议尽量使用生成器表达式和函数,它们采用惰性(非严格)方式迭代对集合元素求值,按需进行计算。由于迭代器只能使用一次,有时必须将集合实例化为元组对象或者列表对象。实例化集合耗费内存和时间,所以只在必要的时候这么做。

熟悉Clojure的开发者不难发现,Python的惰性生成器与Clojure的lazy-seqlazy-cat函数是对等的,它们提供了相同的功能:开发者可以定义一个无限序列,仅在需要的时候从中取值。

使用状态映射

Python提供了几个有状态的集合数据结构,包括字典类和collections模块中定义的映射数据结构。这些数据结构都是有状态的,使用的时候要小心。

就本教程主题而言,映射主要有两个使用场景:将映射聚合在一起的有状态字典和不可变字典。本章的第一个例子演示了ElementTree.findall()方法如何使用不可变字典。Python没有提供简单易用的不可变映射数据结构,虽然collection.abc.Mapping类是不可变的,但不易使用。

在不考虑使用collections.abc.Mapping抽象类的情况下,可以采取一些简单的变通方法保证数据的不可变性质,包括映射变量(不妨叫它ns_map)只出现在赋值语句左边一次,避免使用ns_map.update()或者ns_map.pop()方法,不在del语句中使用映射变量等。
有状态的字典数据结构主要有如下两个使用场景。

  • 只创建一次,且不再更新。在这种情况下,通常使用dict类的散列键功能提高性能。通过dict(seqence)函数,可以基于任何(key, value)组成的可迭代序列创建字典。
  • 叠加式构造字典。使用它避免对列表对象进行实例化和排序。第6章会通过分析collections.Counter类讲解高级归约技术。另外,在记忆化技术中,也常用叠加式构造。

作为上面使用场景的第一个例子,“只创建不修改”这种方式主要适用于三段式处理应用:收集输入,创建一个字典对象,最后根据字典中的映射处理输入。这类场景的一个常见应用是在图像处理中用(R, G, B)元组指定颜色值。当使用GIMP(GNU Image Manipulation Program)文件格式时,调色板定义如下所示:

GIMP Palette
Name: Small
Columns: 3
#
  0   0   0    Black
255 255 255    White
238  32  77    Red
28  172 120    Green
31  117 254    Blue

首先,定义一个名为Color的命名元组:

from collections import namedtuple
Color = namedtuple("Color", ("red", "green", "blue", "name"))

接下来,假设我们用解析器生成了由Color对象组成的解析器,如果将其实例化,转换成的元组如下所示:

(Color(red=239, green=222, blue=205, name='Almond'),
 Color(red=205, green=149, blue=117, name='Antique Brass'),
 Color(red=253, green=217, blue=181, name='Apricot'),
 Color(red=197, green=227, blue=132, name='Yellow Green'),
 Color(red=255, green=174, blue=66, name='Yellow Orange'))

为了能根据颜色名称快速定位到颜色元组,需要基于该序列创建一个不可变字典。当然,这不是根据名称快速定位的唯一方法,后面会介绍其他方法。

从元组创建映射,需要使用之前讲过的process(wrap(iterable))设计模式。创建颜色映射如下所示:

name_map = dict((c.name, c) for c in sequence)

其中序列变量是前面提到的Color对象,模式中的wrap()步骤将每个Color对象转换为元组(c.name, c)process()步骤使用dict初始化方法生成一个从名称到Color对象的映射。转换结果如下所示:

{'Caribbean Green': Color(red=28, green=211, blue=162,
 name='Caribbean Green'),
 'Peach': Color(red=255, green=207, blue=171, name='Peach'),
 'Blizzard Blue': Color(red=172, green=229, blue=238, name='Blizzard Blue'),
 etc.
}

由于字典中元素的顺序是随机的,所以运行结果中Caribbean Green可能不在第一位。

这样就完成转换了,生成的字典对象后续将多次用于从名称到(R, G, B)色值的转换过程中。由于字典在查找元素时首先将键值转换为散列值,所以查找速度会非常快。

使用bisect模块创建映射

前面的例子通过创建字典实现了从颜色名称到Color对象的快速映射。除此之外,还可以使用bisect模块来实现。使用bisect模块需要先创建一个有序对象,然后才能搜索。为了保持与字典映射结构兼容,可以使用collections.abc.Mapping作为基类。

字典映射使用散列值搜索元素,速度非常快,但需要消耗大量内存。相比而言,bisect映射的搜索速度也很快,并且使用的内存较少。

下面定义一个静态映射类:

import bisect
from collections import Mapping
from typing import Iterable, Tuple, Any
class StaticMapping(Mapping):
    def __init__(self,
      iterable: Iterable[Tuple[Any, Any]]) -> None:
        self._data = tuple(iterable)
        self._keys = tuple(sorted(key for key, _ in self._data))
    def __getitem__(self, key):
        ix = bisect.bisect_left(self._keys, key)
        if ix != len(self._keys)
          and self._keys[ix] == key_:
            return self._data[ix][1]
        raise ValueError("{0!r} not found".format(key))
    def __iter__(self):
        return iter(self._keys)
    def __len__(self):
        return len(self._keys)

这个类从抽象超类collections.abc.Mapping派生而来,它定义了新的初始化方法,并增加了基类中没有的3个方法。Tuple[Any, Any]定义了一个代表普遍情形的二元组。

__getitem__()方法用bisect.bisect_left()函数搜索集合内的所有键,如果找到了目标键,则返回相应结果。__iter__()方法返回超类需要的迭代器,__len__()方法与之类似,返回整个集合的长度。

或者参照collections.OrderedDict类的代码,把超类从MutableMapping改成Mapping,并移除能修改状态的所有方法。

更多相关信息,可访问https://docs.python.org/3.3/library/collections.abc.html#collections-abstract-base-classes。

这个类并没有严格遵循函数式编程的原则,我们的目标是为大型应用提供一种尽量少使用有状态变量的方法,这个类保存了键值对的静态集合。作为优化手段,它实例化了两个对象。

通过实例化对象,使用这个类的应用能实现快速键查找。它的超类不允许更新对象,所以整个集合是无状态的。它的速度没有内置的字典快,但使用内存少。由于这个类的基类是Mapping,可以想见这个类的实例不是用于存储过程状态的。

使用有状态的set

Python提供了多种有状态的集合,包括set集合。本书中set有两个主要的使用场景:用有状态的set收集数据,或者用不可变set (frozenset)优化数据搜索算法。

基于可迭代对象创建frozenset跟通过fronzenset(some_iterable)方法创建元组对象的方法类似。创建的集合可以执行快捷in操作。整个处理过程是:收集数据,创建set,用此frozenset处理其他数据。

有时会用一组颜色进行色键处理,也就是借助某个颜色生成的遮罩来合并两张图片。实践中,使用单一颜色的色键效果不理想,而使用一组相近的颜色效果会好很多,这时就需要判断一个图像文件的每个像素是否在色键集合中。在这种情况下,通常的做法是在处理目标文件之前,先把所有的色键颜色放入一个frozenset里。关于色键处理的更多信息,可参考维基百科词条Chromakey。

对于映射(尤其是Counter类),使用记忆化数值往往能大幅提高算法性能。有些函数运用记忆化技术提高性能,因为它在域值和范围值之间做映射,而这恰好是映射数据结构的专长。另一些算法则在处理数据的过程中维护不断积累的记忆化数据集合,以此提高性能。

Python教程

Java教程

Web教程

数据库教程

图形图像教程

大数据教程

开发工具教程

计算机教程