Python极简讲义:一本书入门数据分析与机器学习
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

3.5 常用的内建模块

前面我们讨论了如何自建模块。实际上,在Python中,有很多好用的内置(build-in)模块,很多时候,合理地使用它们能让我们的开发效率大幅提高。下面我们就挑选几个常用的模块进行介绍。

3.5.1 collections模块

通过前面的介绍,我们知道,Python拥有一些内置的数据类型,如Number(数值型)、Str(字符串型)、List(列表)、Tuple(元组)、Dict(字典)等,collections(容器)模块基于这些基础数据类型,“站在巨人的肩膀上”提供了几个有用的数据类型。

在Python官方文档中,collections的定位是“高性能容量数据类型”(High-performance container datatypes),其主要数据类型包括但不限于如下五类。

●namedtuple:生成可以使用名字来访问元素内容的元组子类,可理解为加强版的元组。

●deque:双向队列,可从另一侧高效添加和弹出元素,是列表类的有效补充。

●OrderedDict:有序字典,是字典类提供排序功能的定制版。

●defaultdict:带有默认值的字典。

●Counter:计数器,主要用来对某些数据类型(如列表、元组等)中的元素进行计数。

下面分别给予简单介绍。

3.5.1.1 namedtuple

如前所述,元组可视为列表的常量版本,它是一种不变数据类型。创建元组时,在圆括号“()”之内添加元素,并用逗号将不同元素隔开即可。例如,一个点的二维坐标可以如下表示。

但是,如果仅仅看到数值(3, 4),由于缺乏可读性,我们很难看出这个元组表示的是一个二维空间的坐标点。当然,我们可以把这个坐标封装为一个可读性很好的类(在后续的章节中,我们会讨论面向对象编程涉及的议题),但这又有点“大张旗鼓”,折腾劲太大,性价比不高。这时,namedtuple就有用武之地了。顾名思义,namedtuple就是“命名版的元组”。创建命名元组的示例代码如下。

在In [2]处,我们先得从模块collections中将namedtuple导入,它可以返回一个新的元组子类,利用这个子类可以创建一个自定义的命名元组对象。

为了构造这样一个子类,namedtuple的构造方法需要两个参数,分别是元组子类的名字(In [3]处括号内的Point)和其属性的名字。多个属性可以用列表的方括号括起来,不同属性用逗号隔开,如In [3]处的['x', 'y']。

In [4]处这个命名给出了元组的实例p。这时,Point可理解为一个被namedtuple加工出来的简易版的类,然后可以用属性(通过“对象名.属性”的方式)而非索引来访问这个命名元组Point中的某个元素(见In [5]和In [6])。

由于加上了命名这个特性,代码更易于维护。它既保留元组元素的天然属性——不可变性,又具备由命名带来的可读性,二者结合,相得益彰,十分方便。

实际上,namedtuple是一个工厂类。什么是工厂类呢?简单来说,通过它“加工”出来的依然是元组的子类,只不过不同的类“各有个性”罢了。这个过程有点类似于,由于加工参数不同,工厂使用相同的原材料可以生产出略有不同的零部件,但本质上,这些零部件属于同一个派系。因此,从继承派系上来看,namedtuple加工出来的个性化的类(如上述代码In [3]处生成的Point)依然是元组的子类。我们可以通过如下代码验证创建的对象p是否为元组和Point的实例(instance)。

在In [4]处,对象p是由类Point定义出来的,所以p自然是类Point的一个实例。这里用到了Python的一个内置函数isinstance()。该函数的功能是判断一个对象是否为一个已知类型的实例。

从上面的输出可以看到,isinstance()还可以做到“隔代指认”。严格来讲,Point应该属于元组的子子类,但依然被认为是元组的一个实例。

既然元组和namedtuple有这么密切的关系,二者必然有很多相通的地方。事实也的确是这样,namedtuple还有一个重要的优点,就是它与元组是完全兼容的。也就是说,我们依然可以用索引下标去访问一个namedtuple元素,示例如下。

甚至我们还可以像普通元组一样解包(unpacking)namedtuple中的元素。解包是Python的特有属性,其表现形式为,把一个包含多个元素的对象(如列表、元组等)一次性地赋值给多个简单变量,对象内部的元素会被解开,并按照位置顺序,一一赋值给简单变量。

3.5.1.2 deque

使用列表存储数据时,如果按索引访问元素,即执行只读操作,访问速度会很快。因为列表是线性存储的,因此列表元素的插入和删除操作(即写操作)就很慢。特别是当列表元素数据量很大时,插入和删除操作的效率简直低得令人难以容忍。

deque(双向队列)是为了实现高效插入和删除操作的数据类型,它特别适用于队列和栈的操作,示例代码如下。

上述代码仅仅演示了deque的部分增、删、改、查功能。有了这些好用辅助方法,我们就可以在双向列表中高效添加或删除元素了。

3.5.1.3 OrderedDict

我们知道,普通的字典是由一系列的键/值对构成的。在使用字典时,键是无序的。因此,在对字典对象做迭代时,由于无法确定关键字key的顺序,因此会带来操作上的不便。

如果想要保持关键字key的顺序,可以使用“定制版”的字典——有序字典(OrderedDict)。有序字典的底层是通过双向链表来实现的,内部通过map()函数对指定字典元素序列做映射,以高效存储“键/值对”,示例代码如下。

需要注意的是,OrderedDict中的“键”是按照元素插入的顺序来排列的,而不按照键本身排序,我们可以用如下代码输出有序的键。

OrderedDict中有很多方法,下面我们仅挑选几个方法(update()、pop()、move_to_end())来说明用法。先介绍一下update()方法的作用,该方法用于向老字典od之中追加一个新字典,实际上,相当于合并了两个字典。

下面说明pop()和move_to_end()方法的用法。

3.5.1.4 defaultdict

使用字典时,如果所引用的键不存在,就会抛出异常——KeyError,从而导致整个程序终止执行。如果希望键不存在时能返回一个默认值,就需要使用提供默认值的字典类型defaultdict。

需要注意的是,字典defaultdict的默认值是在某个键缺位时才会返回的值,这个“默认补位”的值需要在创建defaultdict对象时传入。在In [2]处,我们使用了一个匿名函数lambda来设置默认值。除了在键不存在时返回默认值,defaultdict的其他行为与普通字典类型并无二样。

3.5.1.5 Counter

Counter一词的中文含义就是“计算器”,在Python中它是collections包中提供的一个简易计数器类。例如,如果我们想统计某个单词出现的频率,一种简单的办法就是将单词作为字典的“键”,而将次数作为字典的“值”,然后用for循环轮询单词列表,每遇到同一个单词就让值+1。其代码如【范例3-2】所示。

【范例3-2】利用字典统计词频(for-loop-count.py)

【运行结果】

现在我们用Counter来实现与【范例3-2】相同的功能,参见【范例3-3】。

【范例3-3】利用Counter统计词频(Counter-yellow.py)

【运行结果】

【代码分析】

【范例3-3】的第01行导入了Counter类请注意:在Python中有这样的潜在命名规则,如果导入部分的首字母是大写的,通常表示一个类;如果导入部分的首字母是小写的,通常表示一个函数。。第02行定义了一个列表colors,第03行创建一个Counter对象result,colors是Counter计数的数据源。第04行输出统计结果。需要注意的是,Counter并不能直接输出,必须显式地将Counter对象result转换为字典,才能正常输出。

显然,通过使用Counter,代码更加简单了,也更加易读和易于维护了。

Counter类中最常用的方法莫过于most_common(n)了,这里的n表示某个数字,它表示出现频率最高的几个对象,它以列表中内嵌元组的形式出现,每个元组对象由两部分构成,前者是对象,后者是对象出现的频率。例如,如果我们想返回出现频率最高的两个单词,可以在【范例3-3】中添加如下代码。

于是,运行的结果中就多出了如下内容。

上述结果表示,单词red出现了2次,单词blue出现了2次。如果我们想读取blue的数量,则可按照层次解析的方法应用如下语法。

上述语句看起来有点复杂,为什么会写成这番怪模样呢?但如果你对Python面向对象的语法比较熟悉,就不难理解。

首先,如前所述,result.most_common(2)方法返回的是一个列表对象,其中包含两个元素(每个元素又是一个元组)。现在如果我们想提取第1个(从0开始计数)元素,那么就需要写成result.most_common(2)[1]这样的形式,这个操作返回的依然是一个元组对象,这个元组里依然有两个元素,一个是'blue',一个是2。现在我们想读取第1个(从0开始计数)元素,那么就得再添加一层方括号,自然就是result.most_common(2) [1][1]这样了。

3.5.2 datetime模块

顾名思义,datetime模块是Python处理日期和时间的标准库,它就是date和time模块的结合。该模块提供了多种操作日期和时间的类,在支持日期、时间数学运算的同时,重点聚焦于如何能够更高效地支持日期的格式化输出。

3.5.2.1 获取当前时间

我们先来看看如何获取当前日期和时间。

对于In [1]处的代码,需要注意的是,datetime是模块,datetime模块中还包含一个同名的datetime类,我们通过from datetime import datetime导入的才是datetime这个类。

如果仅导入datetime类,则必须引用全名datetime.datetime。如果这样的话,上述In [2]处的代码需要修改为如下形式。

可以通过type()函数来验证now这个对象的身份,见Out[4]处,可以发现datetime.now()返回的是当前的日期和时间,其类型是datetime。

如果要返回特定日期和时间的对象,可以直接用datetime的构造方法来生成这样的对象,方法如下。

我们还可以利用datetime类的属性year、month、day、hour和minute分别输出datetime对象的年、月、日、小时和分钟,示例如下。

3.5.2.2 datetime转换为timestamp

在计算机中,时间实际上是用整型数字表示的。我们把1970年1月1日00:00:00 UTC+00:00时区的时刻称为epoch time(纪元时间),记为0。1970年以前的时间为负数。

我们当前的时间就是相对于纪元时间流逝的秒数,称为timestamp(时间戳)。有了这个时间戳,计算机可以很容易地比较时间的先后。这个时间是机器可读的,但对人而言,理解起来比较困难,因此通常需要转换。

通过如下代码可以方便地查看当前实际的时间戳。

孔夫子有句名言:“逝者如斯夫,不舍昼夜。”这句话形容时间像流水一样不停地流逝。因此当运行In [3]处的代码时,得到的运行结果(时间戳)永远会不一样,因为时间戳永远会单向递增。另外,还需要注意的是,Python中的时间戳是一个浮点数。如果有小数位,小数位表示毫秒。

有时,用户输入的日期和时间是字符串,要处理这样的日期和时间,首先必须把字符串转换为datetime。转换方法并不复杂,可通过datetime.strptime()实现,并需要制定一个日期和时间的格式化字符串,代码如下。

strptime()的第1个参数是日期字符串,很容易理解。这里,最复杂的部分莫过于该函数的第2个参数——变化多端的格式化参数。若格式标记出错,strptime()就难以解析出正确的日期。常见的日期格式如表3-1所示。

表3-1 strptime( )函数中常见的日期格式

3.5.2.3 datetime转换为字符串

如果已经有了datetime对象,我们要把它格式化为字符串显示给用户,转换是通过另外一个函数strftime()实现的。这里,同样需要格式化日期和时间字符串,因此,同样要用到表3-1中列举的格式。

3.5.2.4 datetime加减

有时候,我们需要计算某两个时间或日期的差值,比如说相隔多少个小时,相差多少天等。这时,可以对日期和时间进行加减。这种操作实际上就是向前或向后计算datetime,得到一个新的datetime。

Python提供了很多“语法糖”,对datetime的加减,可以直接用加“+”和减“-”运算符操作。不过,这时需要引入一个特殊的类——时间差类timedelta,示例代码如下。

有了前面知识的铺垫,当我们想计算两个日期相隔多少时,利用时间差类timedelta就比较容易了,参见【范例3-4】。

【范例3-4】计算两个日期之间相隔的天数(gap-days.py)

【运行结果】

【代码分析】

有了时间差类timedelta,我们可以直接输出时间差(如天数days),而无须考虑闰年或闰月等复杂因素(第08行),因为该模块都提前为我们考虑好了。

3.5.3 json模块

JSON(JavaScript Object Notation)是一个受JavaScript的对象字面量语法启发的轻量级数据交换格式。尽管JSON是JavaScript的一个子集,但JSON是独立于语言的文本格式。

用JSON格式描述的数据,可读性很强,其书写格式非常类似于字典,字段名称和值之间用冒号隔开,即“字段名称:值”。

作为Python的一个第三方包名,JSON要用全部小写的形式,即json。json提供了与标准库marshal和pickle相似的API接口。

3.5.3.1 dumps与loads

在Python 3.x中,可以使用json模块来对JSON数据进行编解码,它包含了如下两个函数。

●json.dumps():将Python对象序列化(即编码)为JSON格式的字符串。

●json.loads():将JSON格式的字符串反序列化(即解码)为Python对象。

“dumps”的本意是“倾倒”,这里表示内存信息的转储,它可以把Python的原始类型(如字典、列表等)向JSON类型转换。

“loads”的本意是“装载”,这里表示把JSON类型变换为Python的原始数据类型。参见【范例3-5】。

【范例3-5】利用json模块实现序列化(dumps-json.py)

【运行结果】

【代码分析】

查看第07行,序列化的JSON对象在本质上就是字符串,所以我们可以用print()直接输出结果(第08行)。

我们也可以将一个JSON编码的字符串转换为一个Python数据类型,方法如下。

【运行结果】

【代码分析】

我们可以用type()方法来查看data1的类型,如下。

【运行结果】

由运行结果可以看出,第09行代码成功将一个JSON字符串对象反序列化(解码)为一个字典类型。

3.5.3.2 dump与load

如果我们要处理的是文件而不是字符串,则可以使用json.dump()和json.load()来编码和解码JSON数据(即动词dump和load后面没有字母s),如【范例3-6】所示。

【范例3-6】将列表保持到json文件中(json-file.py)

【运行结果】

运行上述代码之后,在json-file.py相同文件夹下将会出现一个JSON文件data.json,如图3-3所示。

图3-3 JSON文件

【代码分析】

代码的第03~05行定义了一个列表,列表中有一个元素:一个含有多个键/值对的字典。第07行利用open()函数创建了一个文件对象,并取了一个别名为f。这里利用了with语句上下文管理器,执行with语句块后,系统会自动进行资源清理,针对上面的例子而言就是,执行with语句块后,Python系统会自动关闭文件。

类似地,我们可以使用json.load()把数据从JSON文件中读取出来,代码如下。

我们可以把data3中的字典信息读取出来,方法如下。

读取结果如下。

对比图3-3和上面的输出可以发现,JSON编码格式和Python内置的字典类型几乎一样,也有一些细小差异。比如,Python中的True会被映射为true,False会被映射为false,而None会被映射为null。

3.5.4 random模块

Python中的random模块用于生成随机数。下面我们介绍几个random模块中最常用的方法。

3.5.4.1 random()

random模块中有一个同名的方法random()。它用于生成一个0~1之间的随机浮点数。

3.5.4.2 uniform()

上述的random()方法只能返回[0,1)区间的随机数,如果我们要返回指定区间的随机数,该怎么办呢?这时,就需要利用uniform()方法,该方法用于生成一个指定范围内的随机浮点数。该方法有两个参数,其中一个指定上限,另一个指定下限,示例如下。

这里有一个小技巧,如果我们对某个方法不熟悉,在IPython环境下可以通过“?”来查询更多信息,方法如下。

当然,我们也可以利用seed()方法设置随机“种子”,可在调用其他随机模块函数之前调用该方法。这里简要解释一下随机数中“种子”的概念。我们常有这样的比拟:每棵大树,都曾只是一粒种子。也就是说,大树起始于种子。在生成随机数算法时,不论生成多少随机数,都要有一个作为起始条件的数字,这个起始数字就是“种子数”,简称“种子”(seed)。

需要注意的是,一旦设定固定的种子,后续每次产生的随机数都是相同的,反而没有“随机”效果了。如果你想复现上次的随机结果,这种场景下,可以设定随机种子。否则,无须专门设定随机种子。

如果不设置随机种子,Python系统会自行设置如果不设定随机种子,Python会根据系统时间来执行选择,由于每次运行时,系统时间都是不同的,因此生成的随机数也会因时间差异而不同。,基本上可以保证每次产生的随机数都是不同的。为什么说是“基本上”呢?这是因为Python中的random模块将Mersenne Twister作为核心生成器马特赛特旋转演算法是伪随机数生成方法之一。该算法是Makoto Matsumoto(松本)和Takuji Nishimura(西村)于1997年发明的,它可以快速产生高质量的伪随机数,修正传统随机数产生的算法缺陷,因此被广泛使用。,产生的随机数,实际上都伪随机数。

3.5.4.3 randint()

上面产生的随机数都是浮点数(实数),那能不能产生随机整数呢?答案是可以的,这时就要利用random的另外一个方法randint()。

3.5.4.4 randrange()

如果我们想在某个特定产生的序列中随机挑选一个元素,就需要用到randrange()方法。randrange()用于在指定范围内按指定基数递增的集合中获得一个随机数,它有三个参数,前两个参数代表范围下限(包含在范围内)和上限(不包含在范围内),第三个参数是递增增量。

我们知道,range()函数可以创建一个整数列表,一般用在for循环中。我们可以很容易地验证如下代码的功能。

【运行结果】

因此,randrange(1, 20, 3)的功能实际上就是在range(1,20, 3)产生的序列中随机挑选一个。

3.5.4.5 choice()

如果我们想从众多元素中选取一个元素,这个元素并不一定是某个数值,而是列表、元组、字典等数据类型中的一个元素,该如何处理呢?这时,我们需要choice()方法来帮忙。

3.5.4.6 choices()

如果想一次性随机挑选多个元素,可以用choices()方法,也就是随机挑选一个元素的choice()方法的复数形式(加上s)。

需要注意的是,choices()方法是“放回采样”的,也就是说某个值被随机选中后,在下一轮中,候选数据集中还有它,它还有可能被再次选中(参见Out[13])。choices()方法相当于调用k次choice()方法,这里的k为choices()方法中设定的随机挑选数量,以上示例中k值为3。

3.5.4.7 sample()

如果我们想一次性随机抽取多个不重复的元素,这时需要用到sample()方法。

sample()方法用于从指定序列中随机获取指定长度的片段,原有序列不会改变。该方法有两个参数,第一个参数代表指定序列,第二个参数是需要获取的片段长度。

3.5.4.8 shuffle()

如果我们想对序列的所有元素进行打乱排序,就需要利用shuffle()方法。shuffle的本意为“混洗、洗牌”,这里也用到了随机的概念,因此也需要random模块导入。