键进行缩进,也有一些 IDE 在键入冒号然后回车后会自动缩进,这样就更方便了。
读者甚至能从用户获取数据然后加以判断,input() 函数可以提示用户输入数据,并转换为字符串。例如,想要获取你喜欢的科幻电影,然后判断两人喜欢的电影是否一致,仅需要添加一句代码,判断流程不需要更改。
In [10]: fiction1 = input("你喜欢的科幻电影是:")
...: if fiction1 == fiction2:
...: print('原来我们都喜欢电影《' + fiction1 + '》')
...: else:
...: print('你喜欢电影《'+fiction1+'》'+',我喜欢电影《'+fiction2+'》')
...:
你喜欢的科幻电影是:环太平洋
你喜欢电影《环太平洋》,我喜欢电影《侏罗纪世界》
4.1.2 嵌套条件结构
在很多情况下,程序需要 2 种及以上的判断,这时嵌套条件结构 if-elif-else 将非常有用。其格式如下:
if condition1:
代码块1
elif condtion2:
代码块2
else
上述代码格式中间的 elif 语句块可以根据实际情况写任意个。
下面来看一个最简单的例子:判断一个数是正数还是负数,并输出判断的结果。
In [13]: number = 2
...: if number < 0:
...: print("{}是一个负数".format(number))
...: elif number > 0:
...: print("{}是一个正数".format(number))
...: else:
...: print("{}既不是正数也不是负数".format(number))
...:
2是一个正数
字符串的 format() 方法可以便利地格式化打印输出。在字符串中,花括号是一个占位符,在语句运行后会替换为 format() 方法中指定的数据。如果在花括号中指定数字(索引),实际输出时它会被映射为 format() 方法中对应的数值。
下面例子的 3 行代码分别展示 format() 方法的 3 种使用方式。
In [15]: print("{}是一个数字".format(2))
...: print("{0}是一个比{1}大的数字".format(10,5))
...: print("{1}是一个比{0}小的数字".format(10,5))
...:
2是一个数字
10是一个比5大的数字
5是一个比10小的数字
4.1.3 单行 if-else
除了将 if-else 写成一个大的语句块,读者还可以直接将它简化为一行。这样的代码更为精炼简洁。
下面依旧用判断一个数是正是负的问题进行了代码演示。
In [17]: number = 42
...: number_type = '偶数' if number % 2 == 0 else '奇数'
...: print("{} 是一个 {} ".format(number, number_type))
...:
42 是一个 偶数
4.1.4 使用逻辑操作符
在前面的章节本书已经见到介绍了逻辑操作符以及一些相关函数,如 and 逻辑操作符、any() 函数。这些操作常用于条件测试中,下面对常见操作与方法进行汇总和实例说明。
逻辑操作符:
and 是逻辑与。
or 是逻辑或。
not 是逻辑非。
逻辑函数:
all() 在参数全真时返回结果为真(True)。
any() 在只要有 1 个参数为真时,返回结果为真(True)
利用上述提到的逻辑操作符,读者可以构建任意复杂的条件测试。
例如,判断一个数是不是 2 和 5 的公倍数,可以使用以下代码:
In [18]: number = 10
...: if number % 2 == 0 and number % 5 == 0:
...: print("数字{}是2和5的公倍数".format(number))
...:
数字10是2和5的公倍数
再构建一个稍微复杂的条件判断,检测一个数既不能被 2 整除又小于 10:
In [19]: number = 22
...: if (not number % 2 == 0) and (number < 10):
...: print(number)
...: else:
...: print("输入的数不满足条件")
...:
输入的数不满足条件
上面使用了 not 操作符对 number % 2 == 0 的结果取反,即不能被 2 整除。因为涉及嵌套逻辑,所以使用英文括号分隔逻辑判断使得整体层次更为清晰易读。
当条件测试项非常多时,可以使用 all() 和 any() 函数进行简化。
#-------------- 判断所有条件全为真时
# 普通写法
if condition1 and condition2 and condition3 and ...
# 使用 all() 函数
if all(condition1, condition2, condition3, ...)
#-------------- 判断任一条件为真时
# 普通写法
if condition1 or condition2 or condition3 or ...
# 使用 any() 函数
if any(condition1, condition2, condition3, ...)
all() 和 any() 函数在进行向量化计算和判断时极为有用,例如,同时判断列表的所有元素是否都大于 2。因为 Python 列表不支持向量化计算,本书在介绍 Numpy 时再举例说明。
in 操作符在判断某个元素是否存在于序列(列表、元组、集合等)中时也十分有用。
In [23]: 1 in [1, 2, 4, 5]
Out[23]: True
因为 in 操作符能够判断序列(列表、元组、字典等)的成员是否存在,因此也常用于条件测试中。
In [24]: if 2 in [1,2,3,5,7,9]:
...: print("这个列表肯定不全是奇数,因为包含了数字2")
...:
这个列表肯定不全是奇数,因为包含了数字2
现在读者已经学习了如何使用条件结构,下一节开始介绍最常见循环结构 for 语句。
4.2 for 语句
for 语句是最为常见的循环语句,它在一个可迭代(列表、元组等)对象上逐一提取其中的元素。这一语句使得重复性的结构代码可以被有效地缩减。
4.2.1 for 语句块
for 语句的格式如下:
for 迭代变量 in 序列
例如,想要输出 1~100 这 100 个数字,如果事先不知道循环结构,读者需要连续输入 100 条 print 语句!
print(1)
print(2)
print(3)
...
print(100)
一旦使用 for 循环,仅需要以下两行语句:
In [25]: for i in range(1, 101):
...: print(i)
...:
1
2
3
4
5
6
7
8
9
10
...
100
for 语句中的变量 i 称为迭代变量,它依次存储序列 range(1,101) 里的所有元素。当读者使用 print() 函数逐一打印变量 i 的值时,相当于逐次打印序列中的元素。
注意,range() 函数右侧区间不包含(左闭右开),即 range(1,101) 指从 1 到 100,包含 1,不包含 101。这与在前一章介绍的列表切片索引方式是一致的。
4.2.2 else 语句块
在 Python 的 for 循环中,也可以使用 else 语句块。
例如,打印数字 1~5,然后输出循环结束了。
In [26]: for i in range(1, 6):
...: print(i)
...: else:
...: print("For循环结束了。")
...:
1
2
3
4
5
For循环结束了。
不过想要输出这样的结果并不需要 else 语句的参与,下面的代码也可以实现。
In [27]: for i in range(1,6):
...: print(i)
...:
...: print("For循环结束了")
...:
1
2
3
4
5
For循环结束了
注意,语句 print(“For循环结束了”) 并不属于 for 循环结构(不然每次输出数字也会跟着输出一次),两者中间需要空一行 Python 才能区分。
4.2.3 索引迭代
for 循环能够方便地提取一个序列中的元素。但有时候,读者不仅需要知道序列的元素,还想知道元素的位置,该怎么办呢?
Python 提供了 enumerate() 函数可以在 for 循环中同时操作元素与索引。一个简单的示例如下:
In [28]: for n, x in enumerate('亲爱的你好吗?'):
...: print(n, x)
...:
0 亲
1 爱
2 的
3 你
4 好
5 吗
6 ?
enumerate() 函数可以指定一个起始点参数 start。例如,将起始点设为 1,这样输出的索引值可能更符合某些读者的感官。
In [29]: for n, x in enumerate('亲爱的你好吗?', start=1):
...: print(n, x)
...:
1 亲
2 爱
3 的
4 你
5 好
6 吗
7 ?
可以看到,虽然输出的字符串序列还是跟前面的一样,但索引值由 0 到 6 变更为了 1 到 7。
4.2.3 多列表迭代
有时候,我们会想同时对多个列表进行操作,zip() 函数提供了简便实现多列表元素同时迭代循环的方法。
下面用一个非常简单的实例说明——对两个列表索引值对应的元素相加。
In [30]: odd = [1, 3, 5]
...: even = [2, 4, 6]
...: for i, j in zip(odd, even):
...: print("和为{}".format(i+j))
...:
和为3
和为7
和为11
4.2.4 列表推导式
列表推导式(有时候也称列表生成式)是非常 Python 化的循环方式,它不仅体现着 Python 简洁优美的思想,而且比普通的循环方式更容易读懂和节省时间。
如果想要对列表所有的数值(1~100)求平方,利用学习过的 for 循环,读者可能会编写类似下面的代码解决问题。
numbers = list(range(1,101))
result = []
for num in numbers:
result.append(num * num)
列表推导式的写法如下:
result = [num * num for num in numbers]
可以看到,for 关键字的右侧是已经学习过的 for 循环结构,而左侧是想要在 for 循环中执行的操作。
为了让读者对两种写法的效率有更清楚的认识,本书以两种方式求取 1~100000的 平方值,并使用 time 模块计算运行时间。
import time
numbers = list(range(1,100001))
fl_square_numbers = []
# 计算时间
t0 = time.perf_counter()
# ------------ for 循环 ------------
for num in numbers:
fl_square_numbers.append(num * num)
# 计算时间
t1 = time.perf_counter()
# ------- 列表推导式 -------
lc_square_numbers = [num * num for num in numbers]
# 执行结果
t2 = time.perf_counter()
fl_time = t1 - t0
lc_time = t2 - t1
improvement = (fl_time - lc_time) / fl_time * 100
# 对结果对齐并设定保留的小数点位数
print("For循环运行时间: {:.4f}".format(fl_time))
print("列表推导式运行时间: {:.4f}".format(lc_time))
print("提升时间: {:.2f}%".format(improvement))
if fl_square_numbers == lc_square_numbers:
print("\n两种计算方式结果相等")
else:
print("\n两种方式计算结果不相等")
代码的执行结果如下:
For循环运行时间: 0.0293
列表推导式运行时间: 0.0082
提升时间: 72.14%
两种计算方式结果相等
上述代码对于初学者可能有些陌生,请不要害怕,它的目的是为了展示列表推导式的效率,在该部分学习中并不要求理解和掌握所有的代码含义。注意,为避免干扰阅读和理解,上面的代码只展示了纯代码或是纯输出结果,没有列出前面序号。
这个例子中,除了列表推导式本身效率的提升,没有调用 append() 方法也节省了大量时间。
4.2.5 条件列表推导式
在基本列表推导式的基础上,读者可以增加条件检测,这样带条件检测的列表推导式称为条件列表推导式。条件语句既可以写在 for 语句块左侧,也可以写在 for 语句块右侧。
条件列表推导式的形式如下:
[ 操作1 if 条件判断 else 操作2 for 迭代变量 in 可迭代对象(列表、元组、字典等) ]
[ 操作 for 迭代变量 in 可迭代对象 if 条件判断 ]
第 1 种形式是在列表推导式中使用 if-else 条件语句,如果为真,则对迭代变量执行操作 1,否则执行操作 2。
第 2 种形式是只在列表推导式中使用 if 语句块,如果为真,则对迭代变量执行相应操作。
为了介绍条件列表推导式的使用方式和执行过程,这里创建一个包含 9 个正整数的列表,利用条件列表推导式将列表中的奇数和偶数分开为单独的列表。
In [34]: numbers = [2, 12, 3, 25, 24, 21, 5, 9, 12]
如果使用 if-else 条件语句,可以写作以下略长的形式:
In [35]: odd_numbers = []
...: even_numbers = []
...: [odd_numbers.append(num) if(num % 2) else even_numbers.append(num) for num in numbers]
...:
Out[35]: [None, None, None, None, None, None, None, None, None]
In [36]: odd_numbers
Out[36]: [3, 25, 21, 5, 9]
In [37]: even_numbers
Out[37]: [2, 12, 24, 12]
基础列表推导式加上条件结构让整个语句显得有些复杂,实际上这个语句从右到左可以分为 3 个步骤:
迭代变量 num 依次获取可迭代变量 numbers 的元素值。
对迭代变量进行条件判断,如果能被 2 整除(余数为 0),则添加元素值到变量 odd_numbers;如果不能被 2 整除(余数不为 0),则添加元素值到变量 even_numbers。
整个列表结果输出,因为元素都被添加到提前声明好的两个变量中了,所以这条语句结果是一个全 None(空)的列表。
通过以上过程读者可以领会到,带 if-else 的列表推导式看起来复杂,其实可以从由右至左读进行理解。这种用法在这里的弊端比较明显,看似只用了一条语句完成了操作,但我们不得的事先声明两个空列表让它们可以使用 append() 方法。并且,列表推导式本身的输出毫无意义。该如何进行有效地简化呢?这里得想办法利用上列表推导式本身的输出结果就是列表这一个点,既然如此,何不将奇数和偶数分别使用条件列表推导式?
相应操作如下:
In [38]: odd_numbers = [num for num in numbers if num % 2]
...: even_numbers = [num for num in numbers if not num % 2]
...:
In [39]: odd_numbers
Out[39]: [3, 25, 21, 5, 9]
In [40]: even_numbers
Out[40]: [2, 12, 24, 12]
解决同样的问题,有时候拆分开来做更简洁易懂。
元组的迭代基本和列表一致,本书不再阐述。除了列表和元组,字典也是频繁被使用和被迭代的对象,下一小节本书将介绍相关的操作。
4.2.6 字典迭代
本书在前一章的字典部分介绍过:keys() 和 values() 方法可以分别获取字典的键与值。关于字典的一个重要特性,读者应当牢记——字典是随机的。因此正确获取元素值的方法一定是通过键进行索引,大部分针对字典的操作都应当同时针对键和值。
Python 提供了 items() 方法用于字典的迭代。假设现在有一个字典,存储着用户喜欢的书籍以及用户对它们的打分,我们可以利用字典迭代输出其内容。
In [41]: books = {"夏洛克*福尔摩斯":98, "哈利波特":80, "达芬奇密码":88}
In [42]: for book_name,book_score in books.items():
...: print(book_name, book_score, sep=":")
...:
夏洛克*福尔摩斯:98
哈利波特:80
达芬奇密码:88
读者如果只想对字典的键或者值进行迭代操作,分别使用 keys() 与 values() 方法即可。
In [43]: for book_key in books.keys():
...: print(book_key)
...:
夏洛克*福尔摩斯
哈利波特
达芬奇密码
In [44]: for book_score in books.values():
...: print(book_score)
...:
98
80
88
在实际的应用中,字典常作为计数器,存储序列(列表、元组)元素出现的次数。无论处理什么样的输入,这类用法都大同小异,有以下步骤:
创建一个空字典
对序列元素进行循环遍历
如果序列元素在字典中,则以该元素为键,对其值加 1
如果序列元素不在字典中,则创建一个新的字典元素,键为该序列元素,值为 1
现在举个例子,如果从某小学随机抽取 10 名学生,分别对它们所处的年级进行计数。
10 名学生的所在年级如下存储在列表中。
In [45]: st_grades = [2, 3, 1, 1, 3, 5, 4, 6, 6, 1]
根据上面描述的步骤,下面的代码实现了对学生所属年级的计数。
In [46]: grades_count = dict() # 初始化字典
In [47]: for st_grade in st_grades:
...: if st_grade in grades_count:
...: grades_count[st_grade] += 1 # 如果某年级已有学生,则对该年级计数加1
...: else:
...: grades_count[st_grade] = 1 # 如果某年级第一次对学生计数,则令该年级的计数为1
...:
In [48]: grades_count
Out[48]: {1: 3, 2: 1, 3: 2, 4: 1, 5: 1, 6: 2}
注意,这里的 grades_count[st_grade] += 1 是 grades_count[st_grade] = grades_count[st_grade] + 1 的简写。同样的,有 -= 和 *= 用于简写。
4.3 while 语句
for 语句可以解决我们常见的 90% 的循环迭代操作需求,但剩下的部分它毫无办法。读者要知道,for 循环的使用是建立在我们已知需要操作的循环次数的基础上。在不知道循环何时停止时,需要借助 while 语句的力量来完成操作。
while 语句的格式如下:
while condition:
语句块
condition 部分是条件检查,结果必须为逻辑值,可以是单个或组合测试语句。需要注意的是,读者在使用 while 语句时,一定要保证循环能够被结束或者跳出,否则程序将进入死循环,软件出现卡死。严重的话,电脑甚至会宕机。不过,游戏开发代码中常用 while 语句保持等待用户输入直到退出。
为了能够展示 while 语句与前面介绍的 for 语句使用条件的差异,这里使用 while 语句完成一个简单的猜数字游戏。
猜数字游戏想必大家不陌生,由一个人作为裁判,选择一个数字,先指定大致范围,由众人猜测,
根据猜测给出高或者低的评价,然后缩小范围,最后猜中的人有特别的奖励。
这里设定数字范围为 0~999,为了随机化取数字,我们利用 random 模块的 randint() 函数生成随机整数。另外,为了让循环及时地停止,使用下面一节学习的 break 语句跳出循环。
import random # 导入random模块
NUMBER = random.randint(0,999) # 生成[0, 999]范围内的数字
while True:
guess = int(input("请输入数字(0-999):\n"))
if guess == NUMBER:
print("恭喜!猜对了!")
break
elif guess > NUMBER:
print("太大了...请重新猜!")
else:
print("太小了...请重新猜!")
本书作者操作的游戏过程如下:
请输入数字(0-999):
50
太小了...请重新猜!
请输入数字(0-999):
500
太大了...请重新猜!
请输入数字(0-999):
200
太大了...请重新猜!
请输入数字(0-999):
100
太大了...请重新猜!
请输入数字(0-999):
60
太小了...请重新猜!
请输入数字(0-999):
80
太大了...请重新猜!
请输入数字(0-999):
70
太大了...请重新猜!
请输入数字(0-999):
60
太小了...请重新猜!
请输入数字(0-999):
65
太大了...请重新猜!
请输入数字(0-999):
64
太大了...请重新猜!
请输入数字(0-999):
63
太大了...请重新猜!
请输入数字(0-999):
62
恭喜!猜对了!
合适地使用 while 语句可以极大地减少使用循环结构时的需要的初始认知。代码不需要知道要运行多少次,只需要知道当某种条件触发时,它会立即停止。不过有时候这需要接下来一小节介绍的 continue 和 break 命令的配合。
4.4 continue、break 与 pass
有时候,存在类似这样的问题亟待解决:一方面,数据处理的步骤是重复的、繁重的。另一方面,重复似乎没那么有规律,即单纯地使用循环结构不能解决问题。Python 提供了 continue 和 break 语句来帮助扩展循环结构的应用范围,以解决上面所述的问题。
4.4.1 continue
continue 语句可以让当前循环跳过余下的步骤,直接进入下一次循环。
这里举一个简单的例子说明 continue 语句的适用情况。在介绍例子之前,本书先介绍一个相关的数学概念——阶乘。
注意,一个正整数的阶乘(factorial)是所有小于及等于该数的正整数的积,并且 0 的阶乘为 1。自然数 n 的阶乘写作 n!。
例如,2 的阶乘 2! 为 2 X 1 = 2,3 的阶乘 3! 为 3 X 2 X 1 = 6,以此类推。
现在有这样的一个问题:如果现在需要计算从 1 到 10,排除 3 的倍数余下的几个整数的阶乘,即 1、2、4、5、7、8、10 的阶乘,如果用循环实现呢?
当然读者可以使用列表存储这几个数,然后进行循环遍历计算数的阶乘。实现的代码如下:
In [1]: number_list = [1, 2, 4, 5, 7, 8, 10]
In [2]: import math
In [3]: for i in number_list:
...: print(math.factorial(i))
1
2
24
120
5040
40320
3628800
这里本书直接使用了 math 模块中的 factorial() 函数计算阶乘。首先读者需要使用 import 语句导入模块,然后再以模块名后加英文点字符再加函数名的方式调用模块的函数。
完成这个任务好像很简单,这与 continue 语句又有什么关系呢?思考一下,如果我们需要计算的不是简单这几个数的阶乘,而是计算 100 以内,甚至 1000、10000 以内排除 3 的倍数所剩数的阶乘呢?读者还能否使用上述的方式完成任务?
结果是不能。那该如何解决这个问题呢?下面以 1000 以内为例。
上面解法的笨拙之处在于读者需要手动输入列表,实际上它们是有规律的!要计算的 number_list 是一个连续整数除去 3 的倍数的一个列表,如果我们能够通过循环自动创建该列表,那么就不用手动输入列表了,前面的问题就解决了。
In [4]: number_list = []
...: for i in range(1, 1001):
...: if i % 3 == 0:
...: continue
...: else:
...: number_list.append(i)
...:
...: len(number_list)
Out[4]: 667
上面代码使用循环构建所需的 number_list。通过 range() 函数产生数字 1 到 1000,然后当迭代变量 i 是 3 的倍数时,循环转向下一次。当迭代变量 i 不是 3 的倍数时,将数字加入列表 number_list。这样,代码自动生成了后面计算阶乘所需要的列表,避免的手动输入的困难。
条条大路通罗马。如果读者熟练掌握了列表推导式,上述的循环操作便显得多余起来,下面的一行代码就可以实现了,不需要本小节介绍的 continue。
number_list = [ i for i in range(1, 1001) if i % 3 != 0 ]
读者甚至可以对上述的问题用一行代码解答,如下:
[ print(math.factorial(i)) for i in range(1, 1001) if i % 3 != 0 ]
这一行代码的计算量非常大,建议读者不要运行,尝试选择一个小的循环数查看结果。
4.4.2 break
break 语句与 continue 语句不同,一旦循环运行到 break 会跳出循环(即停止循环)而不是转向像 continue 转向下一次循环。在前文使用 while 循环创建的猜数字游戏中,当游戏者猜中数字后,break 语句完成停止游戏的操作。除了游戏,break 在数据分析中使用也十分普遍。当遇到异常的数据或者用户在循环内部设置的测试条件被满足时,break 能够帮助及时地停止循环。
本小节再列举几个使用 break 的简单例子,说明 break 在 while 循环、for 循环以及嵌套循环结构中的应用。
break 语句在 while 循环中的应用
In [5]: a = 1
...: while True:
...: print(a)
...: a += 1
...: if a == 10:
...: break
1
2
3
4
5
6
7
8
9
上述代码使用 while 语句循环打印数字 1~9。while 后接的条件设置的是 True,即无限循环,因而在循环内部通过 if 语句设置条件检测,当变量 a 数值为 10 时运行 break 跳出循环,停止打印。
break 语句在 for 循环中的应用
In [6]: for i in range(5,10):
...: print(i)
...: if i > 7:
...: break
5
6
7
8
上述代码使用 for 语句循环打印 5~8。因为循环迭代的是 range(5,10),即 5~10,所以在循环内部使用 if 语句设置条件检测,当 i 大于 7 时,运行 break 跳出循环,停止打印。
break 语句在嵌套循环中的应用
In [7]: a = 10
...: while a <= 12:
...: a += 1
...: for i in range(1,7):
...: print(i)
...: if i == 5:
...: break
1
2
3
4
5
1
2
3
4
5
1
2
3
4
5
上述代码使用两层嵌套循环重复打印 3 次 1~5。第 1 层循环是 while 语句,第 2 层循环是 for 语句。初始设置变量 a 值为 10,while 循环中代码每一次先令 a 加 1,然后执行嵌套的 for 循环,因为 a 设置为大于 12 则停止循环,所以可知第 1 层循环会执行 3 次。再看内部的 for 语句,迭代变量 i 从 1 到 7 递增,打印 i 的值。当 i 等于 5 时执行 break 语句,此时循环停止。注意,此时 break 语句跳出的是 for 循环,即 break 只能停止所在循环,不能停止外部循环。当需要停止多层循环时,需要使用多个 break 语句。
4.4.3 pass
pass 关键字为 Python 提供了非操作语句,通常作为未执行代码的占位符。因为 Python 需要使用空白字符划定代码块,所以需要 pass 语句进行占位。
读者在构思比较复杂的代码实现时 pass 语句是非常好的帮手,特别是它可以帮助读者检测在某些功能还未完成的情况下,已有功能是否正确实现。例如,下面是一个简单的例子,下面代码实现正负数的判断,但还未曾实现输入是 0 时的处理,因此利用 pass 语句进行占位。
if x < 0:
print('负数!')
elif x == 0:
# 未来要做的事情:....
pass
else:
print('正数!')
4.5 文件操作
数据总是存储在各式各样的文件中,包括文本、数字、图像以及视频,因此对文件进行读写是数据分析人员常见的操作之一。本节将介绍基本的文件类型以及如何使用 Python 内置的模块进行文件基本的读写操作。
4.5.1 文件类型
计算机中的文件通常可以分为两种类型:二进制文件与文本文件。计算机的存储在物理上都是二进制的,所以文本文件与二进制文件的区别并不是物理上的,而是逻辑上的。这两者只是在编码层次上有差异。简单来说,文本文件是基于字符编码的文件,常见的编码有 ASCII 编码、UNICODE 编码等等(现在全世界通用的编码是 UTF-8)。二进制文件是基于值编码的文件,我们可以根据具体应用,指定某个值是什么意思,这个过程也可以看作是自定义编码。
文件类型常常可以通过文件后缀名得知。后缀名为 .txt 的文件是典型的文本文件,另外常用于存储表格数据的 csv 文件、xlsx 文件等也都是文本文件,二进制文件则通常是一些可执行的程序软件、图像、视频。在大部分情况下,数据处理的文件都是文本文件。
在磁盘上读写文件的功能都是由操作系统提供的,现代操作系统不允许普通的程序直接操作磁盘,所以,读写文件就是请求操作系统打开一个文件对象(通常称为文件描述符),然后,通过操作系统提供的接口从这个文件对象中读取数据(读文件),或者把数据写入这个文件对象(写文件)。
4.5.2 使用 open() 函数读取文件
Python 提供了 open() 函数用于打开一个文本文件,并返回文件对象,利用该对象 Python 用户能够操作文本。open() 函数常用形式是接收两个参数:文件名(file)和模式(mode),即如果要以读文件的模式打开一个文件对象,除了传入文件路径,还需要以 r 指定为读模式。如果该文件无法被打开,Python 会抛出 OS Error。
为了介绍如何实际读取文本,首先在 Python 工作目录下使用文本编辑器创建一个文本文件,命名为 test.txt,其内容如下:
这是文本的第一行
这是文本的第二行
这是文本的第三行
这是文本的最后一行
在 IPython 中,使用 %pwd 命令即可快速获取当前 Python 的工作目录。本书作者当前的工作目录如下:
In [8]: %pwd
Out[8]: '/Users/wsx'
如果创建的文件在工作目录下,在 IPython 中使用 %ls 命令可以查看当前目录下的文件或文件夹。
In [8]: %ls
Applications/ Library/ Public/ work_script.pbs*
Desktop/ Movies/ go/
Documents/ Music/ test.txt
Downloads/ Pictures/ tmp/
下面使用 open() 函数读取 test.txt 文件。
In [9]: f = open('test.txt', 'r')
这样就成功地打开了一个文件。如果文件不存在,Python 就会抛出一个 IOError 的错误,并且给出错误码和详细的信息告知用户文件不存在。
In [10]: f1 = open('test1.txt', 'r')
---------------------------------------------------------------------------
FileNotFoundError Traceback (most recent call last)
<ipython-input-10-ef17b5d7a1d3> in <module>
----> 1 f1 = open('test1.txt', 'r')
FileNotFoundError: [Errno 2] No such file or directory: 'test1.txt'
当文件打开成功后,接下来用户可以调用 read() 方法一次读取文件的全部内容,Python 会将其表示为一个 str 对象。
In [11]: f.read()
Out[11]: '这是文本的第一行\n这是文本的第二行\n这是文本的第三行\n这是文本的最后一行\n'
文件对象会占用操作系统的资源,并且操作系统同一时间能打开的文件数量也是有限的,因此文件使用完毕后必须关闭,Python 用户读取文件的最后一步是调用 close() 方法关闭文件。
In [12]: f.close()
注意,文件读写可能产生 IO Error,一旦发生这种情况,后面使用 f.close() 关闭文件就不起作用了。为了保证无论发生什么情况都能正确关闭文件,读者可以使用 try…finally 语句块来实现文件的正确关闭。例如,读者可以使用下面代码来确保读取上面的 test.txt 异常时能够关闭文件。
try:
f = open('test.txt', 'r')
print(f.read())
finally:
if f:
f.close()
只需要修改文件名(包括路径),上述代码即可应用于任何文本文件的读取。但是每次都这么写实在繁琐,因此 Python 引入了 with 语句来自动帮助用户自动调用 close() 方法。
with open('test.txt', 'r') as f:
print(f.read())
上述代码与 try…finally 效果一致,但明显更为简洁,并且不必显式调用 close() 方法。
虽然 read() 方法可以一次性将文件所有内容读取进来,但是实际使用的并不多。如果文件过大,超过计算机内存限制,不仅文件内容不能完全读进 Python,计算机也容易崩溃。为了保险起见,读者可以反复调用 read(size) 方法,这样每次最多读取 size 个字节的内容。不过这种方式使用的也不多,因为一般处理文件都是按行进行的,因而 Python 提供了 readline() 方法可以每次读取一行内容,而 readlines() 方法则可以一次读取所有内容并按行返回为列表。具体使用什么方法读者需要根据自己的需求决定。
下面代码使用 readlines() 方法将文件内容读取为列表,然后使用 for 循环进行处理。
In [13]: for line in f.readlines():
...: print(line.strip()) # 把末尾的'\n'删掉
这是文本的第一行
这是文本的第二行
这是文本的第三行
这是文本的最后一行
前面介绍的是读取文本文件,并且是 UTF-8 编码的文本文件。如果要读取二进制文件,如图片、视频等等,只需要将读取模式设置为 ‘rb’ 即可。
In [14]: f = open('/Users/wsx/Pictures/cover.png', 'rb')
In [15]: f.read() # 下面输出的结果太多,因此省略
Out[15]: b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00 ...
除了文件类型,文件的字符编码也是经常需要关注的。open() 函数打开文件默认使用 UTF-8 编码,如果要读取非 UTF-8 编码的文本文件,需要给 open() 函数传入 encoding 参数。
例如,中文一般使用 GBK 编码。下面代码读取一个 GBK 编码的文本文件,读者可以看看使用不同的编码参数得到的结果有什么不同。
In [16]: f = open('/Users/wsx/Documents/gbk.txt', 'r', encoding='UTF-8')
In [17]: f.read()
---------------------------------------------------------------------------
UnicodeDecodeError Traceback (most recent call last)
<ipython-input-17-571e9fb02258> in <module>
----> 1 f.read()
/Volumes/Data/miniconda3/lib/python3.6/codecs.py in decode(self, input, final)
319 # decode input (taking the buffer into account)
320 data = self.buffer + input
--> 321 (result, consumed) = self._buffer_decode(data, self.errors, final)
322 # keep undecoded input until the next call
323 self.buffer = data[consumed:]
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xd5 in position 0: invalid continuation byte
In [18]: f = open('/Users/wsx/Documents/gbk.txt', 'r', encoding='gbk')
In [19]: f.read()
Out[19]: '这是GBK编码的文本,如果你不正确解码就看不到正确内容喔~'
4.5.3 使用 open() 写文件
写文件和读文件的步骤是一样的,唯一区别是调用 open() 函数时,读者需要指定模式 ‘w’ 或者 ‘wb’ 表示在对文本文件或二进制文件进行写入操作。
下面演示对前面的 test.txt 写入两行文字,然后再读取进来查看是否成功写入。
In [20]: f = open('test.txt', 'w')
In [21]: f.write('我给文本加一行\n')
Out[21]: 8
In [22]: f.write('我再加一行,这是最后一行')
Out[22]: 12
In [23]: f.close()
读者可以反复调用 write() 方法来将内容写入文件,不过需要注意在最后一定要使用 f.close() 关闭文件,否则将存在丢失数据的可能。这是因为当使用 Python 将内容写入文件时,操作系统不会立即将数据写入磁盘,而是暂时将文本放到内存中缓存,当计算资源空闲时才进行写入,只有调用 close() 方法后,操作系统才会保证把没有写入的数据全部写入磁盘文件。因此,在进行文本写入时使用 with 语句也还是最为保险的方式。
with open('test.txt', 'w') as f:
f.write('我给文本加一行\n')
f.write('我再加一行,这是最后一行')
现在文件已经已经写入了,这时候通过 open() 将内容读取进来查看。
In [24]: with open('test.txt', 'r') as f:
...: for line in f.readlines():
...: print(line.strip())
我给文本加一行
我再加一行,这是最后一行
内容的确成功的写入进了 test.txt 文件,但原先的内容被删除了。仅仅设定写模式 ‘w’ 会首先清空掉文件的内容,然后将要写入的内容写进文件。为了执行文本的追加而不是覆盖操作,读者需要使用 ‘wa’ 来替换 ‘w’,’a’ 为 append(追加)的缩写。所有模式的定义及含义可以阅读 Python 的官方文档。
4.6 章末小结
一个复杂的程序是简单的语句与各类控制循环结构的组合。本章详细介绍了控制结构 if-else 语句,循环结构 for 语句与 while 语句,通过实例介绍了它们的基本操作和较为复杂的嵌套操作,以及应用它们操作常见的列表、元组和字典等序列对象。
除此之外,本章介绍了 Python 基本的文件读写操作,帮助读者构建数据读写的认知体系,也为之后理解更高级的文件操作和处理打下基础。
Back to top