Python 命名元组收集数据

Python 使用命名元组收集数据,将数据收集到复杂数据结构中的第三种方法是使用命名元组,基本的思路是创建一种带名称属性的元组,有如下两种实现方法:

  • 使用collections模块的namedtuple函数;
  • typing模块的NamedTuple基类,这种方式支持类型标示,本书基本上只用这种方法。

前面章节的例子中命名元组类定义如下:

from typing import NamedTuple

class Point(NamedTuple):
    latitude: float
    longitude: float

class Leg(NamedTuple):
    start: Point
    end: Point
    distance: float

这样原来的匿名元组的每个属性都有各自的类型标示了,示例如下:

>>> first_leg = Leg(
... Point(29.050501, -80.651169),
... Point(27.186001, -80.139503),
... 115.1751)
>>> first_leg.start.latitude
29.050501

first_leg对象的类型是NamedTuple类的子类Leg,这个类包含了另外两个命名元组和一个实数。用first_leg.start.latitude就可以获取元组结构的指定数据,这种从前缀函数方式到后缀属性方式的变化,既可以看作一种强调,也可以看作某种让人困惑的语法变化。

Leg()Point()函数代替tuple()函数很重要,它改变了构造数据结构的流程,并且为mypy提供了明确的命名结构来验证类型标示是否正确。

从源数据种创建点对的方法如下:

from typing import Tuple, Iterator, List

def float_lat_lon_tuple(
        row_iter: Iterator[List[str]]
    ) -> Iterator[Tuple]:
    return (
        tuple(*map(float, pick_lat_lon(*row)))
        for row in row_iter
    )

它处理一个迭代器(例如CSV读取器或者KML读取器)给出的一系列字符串,pick_lat_lon()函数从行字符串中取出两个值,map()函数将float()函数应用于这些取出的值,返回结果是普通的元组。

为了创建Point对象,要对上面的函数做如下更改:

def float_lat_lon(
        row_iter: Iterator[List[str]]
    ) -> Iterator[Point]:
    return (
        Point(*map(float, pick_lat_lon(*row)))
        for row in row_iter
    )

Point()构造函数代替了tuple()函数,返回的数据类型也相应地变成了Iterator[Point],表明函数构造的是实数坐标组成的点对象,而不是匿名元组。

可以采用类似的方法构建旅行数据的完整Leg对象,如下所示:

from typing import cast, TextIO, Tuple, Iterator, List
from Chapter_6.ch06_ex3 import row_iter_kml
from Chapter_4.ch04_ex1 import legs, haversine

source = "file:./Winter%202012-2013.kml"
def get_trip(url: str=source) -> List[Leg]:
    with urllib.request.urlopen(url) as source:
        path_iter = float_lat_lon(row_iter_kml(
            cast(TextIO, source)
        ))
        pair_iter = legs(path_iter)
        trip_iter = (
            Leg(start, end, round(haversine(start, end), 4))
            for start, end in pair_iter
        )
        trip = list(trip_iter)
    return trip

整个处理过程由一系列生成器表达式组成。path_iter对象使用两个生成器函数row_iter_kml()float_lat_lon()从KML文件中读取每行文本,提取数据并将其转换为Point对象。pair_iter对象使用legs()生成器函数创建代表每段路径起点和终点的Point对象二元组。

trip_iterPoint二元组转换为最终的Leg对象,list()函数再将这些对象变为一个Leg序列,第4章中定义的haversine()函数负责计算起点和终点间的距离。

cast()函数用于通知mypy工具source对象是TextIO类的一个实例。cast()函数是一个类型标示,不包含运行时操作。由于urlopen()返回值的类型是Union[HTTPResponse, addinfourl],所以cast()函数是必需的。addinfourl对象的类型是BinaryIOcsv.reader()要求输入参数的类型为List[str],需要urlopen()提供文本而非字节。对于简单的CSV文件来说,字节和UTF-8编码的文本的差别不大,使用cast()即可。

为了能正确处理字节,需要使用codecs模块将字节转换为正确编码的文本,如下所示:

cast(TextIO, codecs.getreader('utf-8')(cast(BinaryIO, source)))

最内层的cast()函数用于通知mypy工具source的类型是BinaryIOcodecs.getreader()创建能正确处理UTF-8编码的读取器,这个类的实例基于source对象创建文件读取器。

返回结果是一个StreamReader对象,最外面的cast()函数通知mypy工具将StreamReader作为TextIO的实例来处理。codecs.getreader()创建的读取器是将由字节组成的文件解码为格式正确的文本的关键。其他的类型变换都是提供给mypy工具的类型标示。

trip对象是由Leg实例组成的序列,打印结果如下:

(Leg(start=Point(latitude=37.549016, longitude=
 -76.330295), end=Point(latitude=37.840832, longitude=
 -76.273834), distance=17.7246),
 Leg(start=Point(latitude=37.840832, longitude=-76.273834),
 end=Point(latitude=38.331501, longitude=-76.459503),
 distance=30.7382),
 ...
 Leg(start=Point(latitude=38.330166, longitude=-76.458504),
 end=Point(latitude=38.976334, longitude=-76.473503),
 distance=38.8019))

 需要说明的是,原来的haversine()函数处理的是简单的元组,这里用它来处理命名元组,由于输入参数的顺序没变,所以从元组到命名元组的变化不会影响Python的处理过程。

大多数情况下,使用NamedTuple有助于提高程序的可读性,并能把前缀式函数风格变成后缀式对象风格。

Python教程

Java教程

Web教程

数据库教程

图形图像教程

大数据教程

开发工具教程

计算机教程