第三章 内建数据结构、函数及文件

  • 本章要讨论贯穿本书所要使用的Python语言内建功能。由于像pandas和NumPy这类附加库提供了在大数据集上的高级计算功能,所有它们被设计为与Python内建数据操作工具协同使用。
  • 我们将开始介绍Python的常用数据结构:元组、列表、字典混合集合。然后我们会讨论如何创建可复用的Python函数。我们将介绍Python文件对象的机制以及如何与你的本地文件硬盘交互。

3.1 数据结构和序列

  • Python的数据结构简单但强大。精通这些数据结构是成为优秀Python编程者的必要条件。

元组

  • 元组是一种固定长度、不可变的Python对象序列。创建元组最简单的办法就是用逗号分隔序列值。
    在这里插入图片描述
  • 当你通过更复杂的表达式来定义元组时,通常需要用括号将值包起来,例如下面这个例子。生成了元素是元组的元组:
    在这里插入图片描述
  • 你可以使用tupple函数将任意序列或迭代器转换为元组:
    在这里插入图片描述
  • 元组的元素可以通过中括号[]来获取,在大多数序列类型中都可以使用这个方法。
    在这里插入图片描述
  • 虽然对象元组中存储的对象其自身是可变的,但是元组一旦创建,各个位置上的对象是无法修改的
    在这里插入图片描述
  • 如果元组中的一个对象是可变的,例如列表,你可以在它的内部进行修改:
    在这里插入图片描述
  • 可以使用+号连接元组来生成更长的元组:
    在这里插入图片描述
  • 将元组乘以整数,则会和列表一样,生成含有多分拷贝的元组:
    在这里插入图片描述
  • 请注意对象自身并没有复制,只是指向它们的引用进行了复制。

元组拆包

  • 如果你想要将元组型的表达式赋值给变量,Python会对等号右边的值进行拆包:
    在这里插入图片描述
  • 即使是嵌套元组也可以拆包:
    在这里插入图片描述
  • 使用这个功能,你可以轻易地交换变量名。在其他语言中,代码可能如下:
    在这里插入图片描述
  • 但在Python中,交换可以如下完成:
    在这里插入图片描述
  • 拆包的一个常用场景就是遍历元组或列表组成的序列:
    在这里插入图片描述
  • 另一个常用场景是从函数返回多个值。我将在后续内容详细介绍。
  • Python语言新增了一些更为高级的元组拆包功能,用于帮助你从元组的起始位置“采集”一些元素。这个功能使用特殊的语法*rest,用于在函数调用时获取任意长度的位置参数列表
    在这里插入图片描述
  • rest部分有时是你想要丢弃的数据,rest这个变量名并没有什么特殊之处,为了方便,很多Python编程者会使用下划线(_)来表示不想要的变量:
    在这里插入图片描述

元组方法

  • 由于元组的内容和长度是无法改变的,它的实例方法很少。一个常见的有用方法是count(列表中也可以用),用于计量某个数值在元组中出现的次数:
    在这里插入图片描述

列表

  • 与元组不同,列表的长度是可变的,它所包含的内容也是可以修改的。你可以使用中括号[]或者list类型函数来定义列表:
    在这里插入图片描述
  • 列表与元组非常相似(尽管元组不可修改),他们的很多函数用法是相似的。
  • list函数在数据处理中常用于将迭代器或者生成器转为为列表:
    在这里插入图片描述

增加和移除元素

  • 使用append方法可以将元素添加到列表的尾部:
    在这里插入图片描述
  • 使用insert方法可以将元素插入到指定的位置:
    在这里插入图片描述
  • 插入位置的范围在0到列表长度之间。

inser与append相比,计算代价更高。因为子序列元素不在内部移动为新元素提供空间。如果你想在序列的头部和尾部都插入元素,那你应该探索下collections.deque,它是一个双端队列,可以满足头尾部都增加的要求。

  • insert的反操作是pop,该操作会将特定位置的元素移除并返回:
    在这里插入图片描述
  • 元素可以通过remove方法移除,该方法会定位第一个符合要求的值并移除它:
    在这里插入图片描述
  • 如果不考虑性能,通过使用append和remove,你可以将Python的列表用作一种完全合适的“多集合”数据结构。
  • 使用in关键字可以检查一个值是否在列表中:
    在这里插入图片描述
  • not 关键字可以用作in的反义词,表示“不在”:
    在这里插入图片描述
  • 与字典、集合(后面会介绍)相比,检查列表中是否包含一个值是非常缓慢的。这是因为Python在列表中进行了线性逐个扫描,而在字典和集合中Python是同时检查所有元素的(基于哈希表)。

连接和联合列表

  • 与元组相似,两个列表可以使用+号连接:
    在这里插入图片描述
  • 如果你有一个已经定义的列表,你可以用extend方法向该列表添加多个元素:
    在这里插入图片描述
  • 请注意通过添加内容来连接列表是一个相对高代价的操作,这是因为连接过程中创建了新列表,并且还要复制对象。使用extend将元素添加到已经存在的列表是更好的方式,尤其是在你需要构建一个大型列表时
    在这里插入图片描述
  • 上述实现比下述实现更快:
    在这里插入图片描述

排序

  • 你可以调用列表的sort方法对列表进行内部排序(无须新建一个对象):
    在这里插入图片描述
  • sort 有一些选项偶尔会派上用场。其中一项用于生成一个二级排序Key–一个用于生成排序值的函数。例如,我们可以通过字符串的长度进行排序:
    在这里插入图片描述
  • 下面,我们会讨论sorted方法,该方法可以通过序列产生一个排序后的拷贝。

二分搜索和已排序列表的维护

  • 内建的bisect模块实现了二分搜索和已排序列表的插值。bisect.bisect会找到元素应该被插入的位置,并保持序列排序,而bisect.insort将元素插入到相应位置
    在这里插入图片描述

bisect模块的函数并不会检查列表是否已经排序,因为这么做代价太大。因此,对未排序列表使用bisect的函数虽然不会报错,但可能会导致不正确的结果。

切片

  • 使用切片符号可以对大多数序列类型选取其子集,它的基本形式是将start:stop传入符号[ ]中:
    在这里插入图片描述
  • 切片还可以将序列赋值给变量:
    在这里插入图片描述
  • 负索引可以从序列的尾部进行索引:
    在这里插入图片描述
  • 如果你以前使用过R或MATLAB,切片语义则需要适应一哈。图3-1有助于理解通过正数或负数进行切片。在图中,索引值被显示在“箱体边缘”,有助于展示使用正数或负数进行切片选择的起始位置和结束位置。
  • 步进值step可以在第二个冒号后面使用,意思是每隔多少个数取一个值:
    在这里插入图片描述
  • 当需要对列表或元组进行翻转时,一种很聪明的用法就是向步进传值-1
    在这里插入图片描述

3.1.3内建序列函数

enumerate
  • 我们经常需要在遍历一个序列的同时追踪当前元素的索引。一种自行实现的方法像下面的示例:
    在这里插入图片描述
  • 由于这种场景很常见,所以Python内建了enumetate函数,返回了(i,value)元组的序列,其中value是元素的值,i是元素的索引:
    在这里插入图片描述
  • 当你需要对数据建立索引时,一种有效的模式就是使用enumerate构造一个字典,将序列值(假设是唯一的)映射在索引位置上:
    在这里插入图片描述
sorted
  • sorted函数返回一个根据任意序列中的元素新建的已排序列表:
    在这里插入图片描述
  • sorted函数接受的参数与列表的sort方法一致。
zip
  • zip将列表、元组或其他序列的元素配对,新建一个元组构成的列表:
    在这里插入图片描述
  • zip 可以处理任意长度的序列,它生成列表长度由最短的序列决定:
    在这里插入图片描述
  • 给定一个已“配对”的序列时,zip函数有一种机智的方式去“拆分”序列。这种方式的另一种思路就是将行的列表转换为列的列表。语法看上去略显魔幻:
    在这里插入图片描述
reversed
  • reversed函数将序列的元素倒序排列:
    在这里插入图片描述
  • 请牢记,reversed是一个生成器(将在后续内容讨论),因此如果没有和实例化(例如使用list函数或进行for循环)的时候,它并不会产生一个倒序的列表。

3.1.4字典

  • dict(字典)可能是Python内建数据结构中最重要的。它更为常用的名字是哈希表或者是关键数组。字典是拥有灵活尺寸的键值对集合,其中键和值都是Python对象。用大括号{}是创建字典的一种方式,在字典中用逗号将键值对分隔:
    在这里插入图片描述
  • 你可以访问、插入或设置字典中的元素,就像访问列表和元组中的元素一样:
    在这里插入图片描述
  • 你可以用检查列表或元组中是否含有一个元素的相同语法来检查字典是否含有一个键:
    在这里插入图片描述
  • 你可以使用del关键字或pop方法删除值,pop方法会在删除的同时返回被删的值,并删除键
    在这里插入图片描述
  • keys方法和values方法会分别为你提供字典键、值的迭代器。然而键值对并没有特定的顺序,这些函数输出的键、值都是按照相同的顺序:
    在这里插入图片描述
  • 你可以使用update方法将两个字典合并:
    在这里插入图片描述
  • update方法改变了字典中元素位置,因此对于任何原字典中已经存在的键,如果传给update方法的数据也含有相同的键,则它的值会被覆盖。
从序列生成字典
  • 通常情况下,你会有两个序列想要在字典中按元素配对。起初,你可能会写出这样的代码:
    在这里插入图片描述
  • 由于字典本质上是2-元组(含有2个元素的元组)的集合,字典是可以作为接受一个2-元组的列表作为参数的:
    在这里插入图片描述
  • 稍后我们将会讨论字典推导式,那是另一种构建字典的方法。
默认值
  • 通常情况下,会有这样的代码逻辑:
    在这里插入图片描述
  • 带有默认值的get方法会在key参数不是字典的键时返回None,而pop会抛出异常。一个常见的场景是字典中的值集合通过设置,成为另一种集合,比如列表。举个例子,你可以想象一下将字典组成的列表根据首字母分类为包含列表的字典
    在这里插入图片描述
  • 字典的setdefalult方法就是为了这个目的而产生的。上述的for循环语句可以被写为:
    在这里插入图片描述
  • 内建的集合模块有一个非常有用的类,defaultdict。这个类使得上述目的实现更为简单。想要生成符合要求的字典,你可以向字典中传入类型或能在各位置生成默认值的函数:
    在这里插入图片描述
有效的字典键类型
  • 尽管字典的值可以是任何Python对象,但键必须是不可变的对象,比如标量类型(整数、浮点数、字符串)或元组(且元组内对象也必须是不可变对象)。这里要使用一个术语叫做哈希化,通过hash函数可以检查一个对象是否可以哈希化(即是否可以用作字典的键):
    在这里插入图片描述
  • 为了将列表作为键,一种方式就是将其转换为元组,而元组只要它内部元素都可以哈希化,则它自己也可哈希化:
    在这里插入图片描述

3.1.5集合

  • 集合是一种无序且元素唯一的容器。你可以认为集合也像字典,但是只有键没有值。集合可以有两种创建方式:通过set函数或者用字面值集与大括号的语法:
    在这里插入图片描述
  • 集合支持数学上的集合操作,例如联合、交集、差集、对称差集。考虑一下示例集合:
    在这里插入图片描述
  • 两个集合的联合就是两个集合中不同元素的并集。可以通过union方法或二元操作符完成:
    在这里插入图片描述
  • 交集包含了两个集合中同时包含的元素。可以使用&操作符或intersection方法获得交集:
    在这里插入图片描述
  • 表3-1是常用的集合方法列表。
    在这里插入图片描述
  • 所有的逻辑集合运算都有对应操作,允许你用操作的结果替代操作左边的集合内容。对于大型集合,下面的代码效率更高:
    在这里插入图片描述
  • 和字典类似,集合的元素必须是不可变的。如果想要包含列表型的元素,必须先转换为元组:
    在这里插入图片描述
  • 你还可以检查一个集合是否是另一个结合的子集(包含于)或超集(包含):
    在这里插入图片描述
  • 当且仅当两个集合的内容一模一样时,两个集合才相等:
    在这里插入图片描述

3.1.6 列表、集合和字典的推导式

  • 列表推导式是最受欢迎的Python语言特性之一。它允许你过滤一个容器的元素,用一种简明的表达式转换传递给过滤器的元素,从而生成一个新的列表。列表推导式的基本形式为:
    在这里插入图片描述
  • 这与下面的for循环是等价的:
    在这里插入图片描述
  • 过滤条件是可以忽略的,只保留表达式。例如,给定一个字符串列表,我们可以过滤长度大于2的,并且将字母改为大写:
    在这里插入图片描述
  • 集合与字典的推导式是列表推导式的自然拓展,用相似的方式生成集合与字典。字典推导式如下所示:
    在这里插入图片描述
  • 集合推导式看起来很像列表推导式,只是中括号变成了大括号:
    在这里插入图片描述
  • 和列表推导式类似,集合、字典的推导式非常方便,它们是代码更易读易写。如果有一个字符串的列表,假设我们想要一个集合,集合里包含列表中字符串的长度,我可以通过集合推导式很方便地实现:
    在这里插入图片描述
  • 我们也可以使用map函数更函数化、更简洁化地表达:
    在这里插入图片描述
  • 我们创建一个键字符串与其位置相匹配的字典作为字典推导式的简单示例:
    在这里插入图片描述

嵌套列表推导式

  • 假设我们有一包含列表的列表,内容是英语姓名和西班牙语姓名:
    在这里插入图片描述
  • 你都已经忘了这些名字来自哪些文件,但想要根据语言来组织这些名字。现在再假设我们想要获得一个列表包含所有含有2个以上字母e的名字,我们当然可以简单地使用for循环:
    在这里插入图片描述

假设我们有一个包含列表的列表,包含了一些英文名和西班牙名:

In [161]: all_data = [['John', 'Emily', 'Michael', 'Mary', 'Steven'],
   .....:             ['Maria', 'Juan', 'Javier', 'Natalia', 'Pilar']]

你可能是从一些文件得到的这些名字,然后想按照语言进行分类。现在假设我们想用一个列表包含所有的名字,这些名字中包含两个或更多的e。可以用for循环来做:

names_of_interest = []
for names in all_data:
    enough_es = [name for name in names if name.count('e') >= 2]
    names_of_interest.extend(enough_es)

可以用嵌套列表推导式的方法,将这些写在一起,如下所示:

In [162]: result = [name for names in all_data for name in names
   .....:           if name.count('e') >= 2]
In [163]: result
Out[163]: ['Steven']

嵌套列表推导式看起来有些复杂。列表推导式的for部分是根据嵌套的顺序,过滤条件还是放在最后。下面是另一个例子,我们将一个整数元组的列表扁平化成了一个整数列表:

In [164]: some_tuples = [(1, 2, 3), (4, 5, 6), (7, 8, 9)]
In [165]: flattened = [x for tup in some_tuples for x in tup]
In [166]: flattened
Out[166]: [1, 2, 3, 4, 5, 6, 7, 8, 9]

记住,for表达式的顺序是与嵌套for循环的顺序一样(而不是列表推导式的顺序):

flattened = []
for tup in some_tuples:
    for x in tup:
        flattened.append(x)

你可以有任意多级别的嵌套,但是如果你有两三个以上的嵌套,你就应该考虑下代码可读性的问题了。分辨列表推导式的列表推导式中的语法也是很重要的:

In [167]: [[x for x in tup] for tup in some_tuples]
Out[167]: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

这段代码产生了一个列表的列表,而不是扁平化的只包含元素的列表。

3.2 函数

函数是Python中最主要也是最重要的代码组织和复用手段。作为最重要的原则,如果你要重复使用相同或非常类似的代码,就需要写一个函数。通过给函数起一个名字,还可以提高代码的可读性。

函数使用def关键字声明,用return关键字返回值:

def my_function(x, y, z=1.5):
if z > 1:
return z * (x + y)
else:
return z / (x + y)
同时拥有多条return语句也是可以的。如果到达函数末尾时没有遇到任何一条return语句,则返回None。

函数可以有一些位置参数(positional)和一些关键字参数(keyword)。关键字参数通常用于指定默认值或可选参数。在上面的函数中,x和y是位置参数,而z则是关键字参数。也就是说,该函数可以下面这两种方式进行调用:

my_function(5, 6, z=0.7)
my_function(3.14, 7, 3.5)
my_function(10, 20)
函数参数的主要限制在于:关键字参数必须位于位置参数(如果有的话)之后。你可以任何顺序指定关键字参数。也就是说,你不用死记硬背函数参数的顺序,只要记得它们的名字就可以了。

笔记:也可以用关键字传递位置参数。前面的例子,也可以写为:

my_function(x=5, y=6, z=7)
my_function(y=6, x=5, z=7)
这种写法可以提高可读性。

命名空间、作用域,和局部函数
函数可以访问两种不同作用域中的变量:全局(global)和局部(local)。Python有一种更科学的用于描述变量作用域的名称,即命名空间(namespace)。任何在函数中赋值的变量默认都是被分配到局部命名空间(local namespace)中的。局部命名空间是在函数被调用时创建的,函数参数会立即填入该命名空间。在函数执行完毕之后,局部命名空间就会被销毁(会有一些例外的情况,具体请参见后面介绍闭包的那一节)。看看下面这个函数:

def func():
a = []
for i in range(5):
a.append(i)
调用func()之后,首先会创建出空列表a,然后添加5个元素,最后a会在该函数退出的时候被销毁。假如我们像下面这样定义a:

a = []
def func():
for i in range(5):
a.append(i)
虽然可以在函数中对全局变量进行赋值操作,但是那些变量必须用global关键字声明成全局的才行:

In [168]: a = None
In [169]: def bind_a_variable():
…: global a
…: a = []
…: bind_a_variable()
…:
In [170]: print(a)
[]
注意:我常常建议人们不要频繁使用global关键字。因为全局变量一般是用于存放系统的某些状态的。如果你发现自己用了很多,那可能就说明得要来点儿面向对象编程了(即使用类)。

返回多个值
在我第一次用Python编程时(之前已经习惯了Java和C++),最喜欢的一个功能是:函数可以返回多个值。下面是一个简单的例子:

def f():
a = 5
b = 6
c = 7
return a, b, c
a, b, c = f()
在数据分析和其他科学计算应用中,你会发现自己常常这么干。该函数其实只返回了一个对象,也就是一个元组,最后该元组会被拆包到各个结果变量中。在上面的例子中,我们还可以这样写:

return_value = f()
这里的return_value将会是一个含有3个返回值的三元元组。此外,还有一种非常具有吸引力的多值返回方式——返回字典:

def f():
a = 5
b = 6
c = 7
return {‘a’ : a, ‘b’ : b, ‘c’ : c}
取决于工作内容,第二种方法可能很有用。

函数也是对象
由于Python函数都是对象,因此,在其他语言中较难表达的一些设计思想在Python中就要简单很多了。假设我们有下面这样一个字符串数组,希望对其进行一些数据清理工作并执行一堆转换:

In [171]: states = [’ Alabama ', ‘Georgia!’, ‘Georgia’, ‘georgia’, ‘FlOrIda’,
…: ‘south carolina##’, ‘West virginia?’]
不管是谁,只要处理过由用户提交的调查数据,就能明白这种乱七八糟的数据是怎么一回事。为了得到一组能用于分析工作的格式统一的字符串,需要做很多事情:去除空白符、删除各种标点符号、正确的大写格式等。做法之一是使用内建的字符串方法和正则表达式re模块:

import re
def clean_strings(strings):
    result = []
    for value in strings:
        value = value.strip()
        value = re.sub('[!#?]', '', value)
        value = value.title()
        result.append(value)
    return result

结果如下所示:

In [173]: clean_strings(states)
Out[173]: 
['Alabama',
 'Georgia',
 'Georgia',
 'Georgia',
 'Florida',
 'South   Carolina',
 'West Virginia']

其实还有另外一种不错的办法:将需要在一组给定字符串上执行的所有运算做成一个列表:

def remove_punctuation(value):
    return re.sub('[!#?]', '', value)

clean_ops = [str.strip, remove_punctuation, str.title]

def clean_strings(strings, ops):
    result = []
    for value in strings:
        for function in ops:
            value = function(value)
        result.append(value)
    return result

然后我们就有了:

In [175]: clean_strings(states, clean_ops)
Out[175]: 
['Alabama',
 'Georgia',
 'Georgia',
 'Georgia',
 'Florida',
 'South   Carolina',
 'West Virginia']
  • 像这种更为函数化的模式可以使你在更高层次上方便修改字符串变换方法。clean_strings函数现在也具有更强的复用性和通用性。
  • 你可以将函数作为一个参数传给其他的函数,比如内建的map函数,可以将一个函数应用到一个序列上:
for x in map(remove_punctuation, states):
    print(x)

3.2.4匿名(Lambda)函数

  • Python支持所谓的匿名或lambda函数。匿名函数是一种通单个语句生成函数的方式,其结果是返回单个语句生成函数的方式,其结果是返回值。匿名函数使用lambda关键字定义,该关键字仅表达“我们声明一个匿名函数”的意思:
def short_function(x):
    return x * 2
equiv_annon = lambda x: x*2
  • 本书的其余部分,我会更偏向使用匿名函数。你将会看到,匿名函数在数据分析中非常方便,因为在很多案例中数据变形函数都可以作为函数的参数。匿名函数都可以作为函数的参数。匿名函数代码量小(也更为清晰),将它作为参数进行传值,比写一个完整的函数或者将匿名函数赋值给局部变量更好。举个例子,考虑下面的不佳示例:
    在这里插入图片描述
  • 你也可以写成 [x * 2 for x in ints],但是在这里我们能够简单地将一个自定义操作符传递给apply_to_list函数。
  • 另一个例子,假设你想要根据字符串中不同字母的数量对一个字符串集合进行排序:
strings = ['foo','card','bar','aaa','abab']
  • 这里我们可以将一个匿名函数传给列表的sort方法:
    在这里插入图片描述

和def关键字声明的函数不同,匿名函数对象自身并没有一个显示的__name__属性,这是lambda函数被称为匿名函数的一个原因。

3.2.5 柯里化:部分参数应用

  • 柯里化是计算机科学术语(以数学家Haskell Curry 命名),它表示通过部分参数应用的方式从已有的函数中衍生出新的函数,其功能是将两个数加一起:
def add_numbers(x,y):
    return x + y
  • 使用这个函数,我们可以衍生出一个只有一个变量的新函数,add_five,可以给参数加上5:
add_five = lambda y: add_numbers(5, y)
  • add_numbers的第二个参数称为“柯里化的”(curried)。这里没什么特别花哨的东西,因为我们其实就只是定义了一个可以调用现有函数的新函数而已。内置的functools模块可以用partial函数将此过程简化:
from functools import partial
add_five = partial(add_numbers, 5)

3.2.6 生成器

  • 能以一种一致的方式对序列进行迭代(比如列表中的对象或文件中的行)是Python的一个重要特点。这是通过一种叫做迭代器协议(iterator protocol,它是一种使对象可迭代的通用方式)的方式实现的,一个原生的使对象可迭代的方法。比如说,对字典进行迭代可以得到其所有的键:
    在这里插入图片描述
  • 当你编写for key in some_dict时,Python解释器首先会尝试从some_dict创建一个迭代器:

在这里插入图片描述

  • 迭代器是一种特殊对象,它可以在诸如for循环之类的上下文中向Python解释器输送对象。大部分能接受列表之类的对象的方法也都可以接受任何可迭代对象。比如min、max、sum等内置方法以及list、tuple等类型构造器:
    在这里插入图片描述
  • 生成器(generator)是构造新的可迭代对象的一种简单方式。一般的函数执行之后只会返回单个值,而生成器则是以延迟的方式返回一个值序列,即每返回一个值之后暂停,直到下一个值被请求时再继续。要创建一个生成器,只需将函数中的return替换为yeild即可
def squares(n=10):
    print('Generating squares from i to {0}'.format(n ** 2))
    for i in range(1,n + 1):
        yield i ** 2
  • 调用该生成器时,没有任何代码会被立即执行:
    在这里插入图片描述
  • 直到你从该生成器中请求元素时,它才会开始执行其代码:
    在这里插入图片描述

生成器表达式

  • 另一种更简洁的构造生成器的方法是使用生成器表达式(generator expression)。这是一种类似于列表、字典、集合推导式的生成器。其创建方式为,把列表推导式两端的方括号改成圆括号:
    -
  • 上面的代码与下面更为复杂的生成器是等价的:
def _make_gen(): for x in range(100): yield x ** 2 gen = _make_gen()
  • 生成器表达式也可以取代列表推导式,作为函数参数:
    在这里插入图片描述

itertools模块

  • 标准库itertools模块中有一组用于许多常见数据算法的生成器。例如,groupby可以接受任何序列和一个函数。它根据函数的返回值对序列中的连续元素进行分组。下面是一个例子:
    在这里插入图片描述
  • 表3-2中列出了一些我经常用到的itertools函数。建议参阅Python官方文档,进一步学习。
    在这里插入图片描述

3.2.7 错误和异常处理

  • 优雅地处理Python的错误和异常是构建健壮程序的重要部分。在数据分析中,许多函数函数只用于部分输入。例如,Python的float函数可以将字符串转换成浮点数,但输入有误时,有ValueError错误:
    在这里插入图片描述
  • 假如想优雅地处理float的错误,让它返回输入值。我们可以写一个函数,在try/except中调用float:
def attempt_float(x):
    try:
        return float(x)
    except:
        return x
  • 如果float(x)执行时抛出了异常,则代码段中的except部分代码将会被执行:
In [200]: attempt_float('1.2345')
Out[200]: 1.2345
In [201]: attempt_float('something')
Out[201]: 'something'
  • 你可能注意到float抛出的异常不仅是ValueError:
    在这里插入图片描述
  • 你可能只想处理ValueError,TypeError错误(输入不是字符串或数值)可能是合理的bug。可以写一个异常类型:
def attempt_float(x):
    try:
        return float(x)
    except(TypeError,ValueError):
        return x
  • 然后有:
In [204]: attempt_float((1, 2))
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-204-9bdfd730cead> in <module>()
----> 1 attempt_float((1, 2))
<ipython-input-203-3e06b8379b6b> in attempt_float(x)
      1 def attempt_float(x):
      2     try:
----> 3         return float(x)
      4     except ValueError:
      5         return x
TypeError: float() argument must be a string or a number, not 'tuple'
  • 可以用元组捕获多个异常:
def attempt_float(x):
    try:
        return float(x)
    except(TypeError,ValueError):
        return x
  • 某些情况下,你可能不想抑制异常,你想无论try部分的代码是否成功,都执行一段代码。可以使用finally:
f = open(path, 'w')
try:
    write_to_file(f)
finally:
    f.close()
  • 这里,文件处理f总会被关闭。相似的,你可以用else让只在try部分成功的情况下,才执行代码:
f = open(path,'w')
try:
    write_to_file(f)
except:
    print('Failed')
else:
    print('Succeeded')
finally:
    f.close()

IPython中的异常

  • 如果是在%run一个脚本或一条语句时抛出异常,IPython默认会打印完整的调用栈(traceback),在栈的每个点都会有几行上下文:
    在这里插入图片描述
  • 自身就带有文本是相对于Python标准解释器的极大优点。你可以用魔术命令%xmode,从Plain(与Python标准解释器相同)到Verbose(带有函数的参数值)控制文本显示的数量。后面可以看到,发生错误之后,(用%debug或%pdb magics)可以进入stack进行事后调试。

3.3文件与操作系统

  • 本书的代码示例大多使用诸如pandas.read_csv之类的高级工具将磁盘上的数据文件读入Python数据结构。但我们还是需要了解一些有关Python文件处理方面的基础知识。好在它本来就很简单,这也是Python在文本和文件处理方面的如此流行的原因之一。
  • 为了打开一个文件以便读写,可以使用内置的open函数以及一个相对或绝对的文件路径:
    在这里插入图片描述
  • 默认情况下,文件是以只读模式(’r’)打开的。然后,我们就可以像处理列表那样来处理这个文件句柄f了,比如对行进行迭代:
for line in f:
    pass
  • 从文件中取出的行都带有完整的行结束符(EOL),因此你常常会看到下面这样的代码(得到一组没有EOL的行):
    在这里插入图片描述
    在这里插入图片描述
  • 原始文本文件如下,我不清楚是为什么会出现这个问题。
    在这里插入图片描述
  • 如果使用open创建文件对象,一定要用close关闭它。关闭文件可以返回操作系统资源:
f.close()
  • 用with语句可以可以更容易地清理打开的文件:
with open(path) as f:
    lines = [x.rstrip() for x in f]
  • 这样可以在退出代码块时,自动关闭文件。
  • 如果输入f =open(path,’w’),就会有一个新文件被创建在examples/segismundo.txt,并覆盖掉该位置原来的任何数据。另外有一个x文件模式,它可以创建可写的文件,但是如果文件路径存在,就无法创建。表3-3列出了所有的读/写模式。
    在这里插入图片描述
  • 对于可读文件,一些常用的方法是read、seek和tell。read会从文件返回字符。字符的内容是由文件的编码决定的(如UTF-8),如果是二进制模式打开的就是原始字节:
    在这里插入图片描述
  • read模式会将文件句柄的位置提前,提前的数量是读取的字节数。tell可以给出当前的位置:
    在这里插入图片描述
  • 尽管我们从文件读取了10个字符,位置却是11,这是因为用默认的编码用了这么多字节才解码了这10个字符。你可以用sys模块检查默认的编码:
    在这里插入图片描述
  • seek将文件位置更改为文件中的指定字节:
    在这里插入图片描述
  • 最后,关闭文件:
f.close()
f2.close()
  • 向文件写入,可以使用文件的write或writelines方法。例如,我们可以创建一个无空行版的prof_mod.py:
    在这里插入图片描述
  • 表3-4是很多常用的文件方法。
    在这里插入图片描述

3.3.1 字节与Unicode文件

  • Python文件的默认操作是“文本模式”,也就是说,你需要处理Python的字符串(即Unicode)。它与“二进制模式”相对,文件模式加一个b。我们来看上一节的文件(UTF-8编码、包含非ASCII字符):
    在这里插入图片描述
  • UTF-8是长度可变的Unicode编码,所以当我从文件请求一定数量的字符时,Python会从文件读取足够多(可能少至10或多至40字节)的字节进行解码。如果以“rb”模式打开文件,则读取确切的请求字节数:
    在这里插入图片描述
  • 取决于文本的编码,你可以将字节解码为str对象,但只有当每个编码的Unicode字符都完全成形时才能这么做:
    在这里插入图片描述
  • 文本模式结合了open的编码选项,提供了一种更方便的方法将Unicode转换为另一种编码
In [236]: sink_path = 'sink.txt'
In [237]: with open(path) as source:
   .....:     with open(sink_path, 'xt', encoding='iso-8859-1') as sink:
   .....:         sink.write(source.read())
In [238]: with open(sink_path, encoding='iso-8859-1') as f:
   .....:     print(f.read(10))
Sueña el r
  • 注意,不要在二进制模式中使用seek。如果文件位置位于定义Unicode字符的字节的中间位置,读取后面会产生错误:
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值