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_iter
将Point
二元组转换为最终的Leg
对象,list()
函数再将这些对象变为一个Leg
序列,第4章中定义的haversine()
函数负责计算起点和终点间的距离。
cast()
函数用于通知mypy工具source
对象是TextIO
类的一个实例。cast()
函数是一个类型标示,不包含运行时操作。由于urlopen()
返回值的类型是Union[HTTPResponse, addinfourl]
,所以cast()
函数是必需的。addinfourl
对象的类型是BinaryIO
。csv.reader()
要求输入参数的类型为List[str]
,需要urlopen()
提供文本而非字节。对于简单的CSV文件来说,字节和UTF-8编码的文本的差别不大,使用cast()
即可。
为了能正确处理字节,需要使用codecs
模块将字节转换为正确编码的文本,如下所示:
cast(TextIO, codecs.getreader('utf-8')(cast(BinaryIO, source)))
最内层的cast()
函数用于通知mypy工具source
的类型是BinaryIO
。codecs.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
有助于提高程序的可读性,并能把前缀式函数风格变成后缀式对象风格。