本文将主要介绍 Python 的语法错误、异常、文件的读取等基础知识。阅读本文预计需要 15 min
错误和异常,以及读取文件,写入文件都是我们经常会遇到的。本文主要内容:
在 Python 中主要分为两种错误:语法错误(syntax errors) 和异常(exceptions)。
语法错误,又称解析错误(parsing errors),指代码的语法不符合 Python 语法规则,导致“翻译官” Python 解释器无法翻译你的代码给计算机,导致会报错。这很常见,尤其是我们初学编程的时候。
语法错误是代码执行前检测到的错误
。
举个栗子:
>>> while True print('Hello world')
File "" , line 1
while True print('Hello world')
^
SyntaxError: invalid syntax
上面就是一个语法错误,学习如何看报错信息。
首先我们看最后一行,SyntaxError: invalid syntax
这句就是告诉我们写的代码出现了 SyntaxError
即语法错误。
继续往上面看,发现 Python 解释器输出了出现语法错误的一行,并且在这句代码有问题的地方(也有可能是这附近,只是在这里检测出来了)用一个箭头 ^
指示,同时再上面一行输出了文件名 "
和出问题代码所在的行数(line),这里是第一行代码出问题了,而且是在 print()函数附近。
通过这样的顺序,我们就可以快速的定位到错误代码的位置,并且知道是什么错误,从而进行修改。这句代码主要是因为 print()函数前面少了一个冒号(:
)。
学会看报错信息,定位错误,对于调试代码非常重要!语法错误相对比较简单,就做这些总结。
除了语法错误,我们更多遇见的是异常(exception)。
有时候,我们的语法没有任何问题,但是在代码执行的时候,还是可能发生错误。这种在代码运行时检测到的错误成为异常
。
下面展示一些常见的异常(来源于 Python 官网):
>>> 10 * (1/0)
Traceback (most recent call last):
File "" , line 1, in <module>
ZeroDivisionError: division by zero
>>> 4 + spam*3
Traceback (most recent call last):
File "" , line 1, in <module>
NameError: name 'spam' is not defined
>>> '2' + 2
Traceback (most recent call last):
File "" , line 1, in <module>
TypeError: Can't convert 'int' object to str implicitly
我们对比一下语法错误和异常的报错信息,以 10 * (1/0)
的报错信息为例:
ZeroDivisionError
即分母为 0 的异常。Traceback (most recent call last):
这句是告诉我们程序在执行过程中追踪到一个错误,下面开始一步一步追踪定位错误。这在语法错误里面是没有的。因为语法错误是在程序执行前检测到的。官网的说法是以堆栈回溯的形式显示发生异常时的上下文,通常它包含列出源代码行的堆栈回溯,但是不会显示从标准输入中读取的行。File "", line 1, in
是告诉我们,文件 ""
模块中的第 1 行代码出问题了。从而我们就定位到了代码出错的位置。可以发现,这里没有出现语法错误的箭头。有时候 Traceback 很长,这时候我们可以先看最后一行,知道错误类型,然后从上往下看报错信息,最终定位到出问题代码的位置(在哪个文件,多少行代码),从而修改代码。
异常的类型很多,这里依次列出了 ZeroDivisionError, NameError 和 TypeError
三种异常,这些异常名称是内置的标识符(identifiers),不是关键字(keywords)。
更多的内置异常(Built-in Exceptions)可以参看官网Built-in Exceptions获取它们的介绍和意义。
Python 中可以用 try...except
语句来处理异常。这有点像 if...else
条件分支语句。
看这个例子:
>>> while True:
... try:
... x = int(input("Please enter a number: "))
... break
... except ValueError:
... print("Oops! That was no valid number. Try again...")
这段代码是要求用户一直输入,直到输入一个有效的整数,但允许用户通过 Ctrl + C 中断程序,这时引发的是 KeyboardIterrut
异常。
下面说明一下 try 语句的工作原理:
如下面这个,except 中什么都不处理,所以程序直接结束,什么都没有输出:
>>> try:
... x = int(input("Please enter a number: "))
... print("good!")
... except ValueError:
... # print("Oops! That was no valid number. Try again...")
... pass
...
Please enter a number: a
>>>
一个 try 语句可以有多个 except 子句,以指定不同异常的处理程序,但是最多会执行一个 except 子句,其他的都会跳过。还有一点要注意,except 子句只会处理 try 子句中发生的异常,对于 except 子句中发生的异常是没有不会被 except 子句处理的。换句话说,就是异常处理程序(except 子句)自身发生异常,try 语句中的 except 都无法处理,交给更高一级处理。如:
>>> try:
... x = int(input("Please enter a number: "))
... print("good!")
... except ValueError:
... # print("Oops! That was no valid number. Try again...")
... print(a)
...
Please enter a number: b
Traceback (most recent call last):
File "" , line 2, in <module>
ValueError: invalid literal for int() with base 10: 'b'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "" , line 6, in <module>
NameError: name 'a' is not defined
>>>
这里可以发现有两个异常,一个是 try 子句引发的 ValueError,还有一个异常处理程序 except 子句中引发的 NameError,这是 try 语句之外的更高一级处理的结果(Python 解释器)。
一个子句也可以将多个异常命名为元组,如:
... except (RuntimeError, TypeError, NameError):
... pass
注意,Python 的错误也是 class,所有错误类型都继承自 BaseException,所以使用 except 时一定要注意,它不但能捕获该类型的错误,还能把其子类也“一网打尽”,如:
class B(Exception):
pass
class C(B):
pass
class D(C):
pass
for cls in [B, C, D]:
try:
raise cls()
except B:
print("B")
except D:
print("D")
except C:
print("C")
输出结果:
B
B
B
简单说,就是 B 是 Exception 的子类,C 是 B 的子类,D 是 C 的子类,所以 raise 抛出错误的时候(raise 待会会说),B、C、B 三种异常类型都可以被 第一个 except B 子句捕获,这样就大致我们后面的 except 子句永远不会生效。所以要注意书写顺序,先子类,再父类,异常的继承顺序可以看前面给的链接。这里正确的做法是把,except B 作为最后一个 except 子句,这样输出结果就是 B、C、D。
try…except 还有一个好处就是可以跨越多层调用,即不仅可以处理 try 子句遇到的异常,还可以处理 try 子句中调用函数发生的异常,如:main()调用 foo(),foo()调用 bar(),结果 bar()出错了,这时只要 main()捕获了,就可以处理:
def foo(s):
return 10 / int(s)
def bar(s):
return foo(s) * 2
def main():
try:
bar('0')
except Exception as e:
print('Error:', e)
main()
输出结果:
Error: division by zero
即我们不需要在每个可能出错的地方去捕获错误,只需要在合适的层次去捕获错误就可以了,这样可以减少写 try…except… 的麻烦。
最后的 except 子句可以省略异常名,来作为通配符(匹配剩余所有的异常),但是这种做法要慎重,因为这种做法很容易掩盖真正的编程错误,让你不知道到底发生了什么异常。此外它也还可以用于打印错误消息,然后重新引发异常(这个很常用,也很有用),如:
import sys
try:
f = open('myfile.txt')
s = f.readline()
i = int(s.strip())
except OSError as err:
print("OS error: {0}".format(err))
except ValueError:
print("Could not convert data to an integer.")
except:
print("Unexpected error:", sys.exc_info()[0])
raise
try…except 语句还有一个可选的 else 子句,在使用时,else 子句必须放在所有的 try 子句后面,如果 try 子句没有引发异常,就会执行,else 子句。else 子句常用在需要向 try 子句添加额外代码的时候。对官网这部分的描述还不是特别理解,这里放一个官网的例子:
for arg in sys.argv[1:]:
try:
f = open(arg, 'r')
except OSError:
print('cannot open', arg)
else:
print(arg, 'has', len(f.readlines()), 'lines')
f.close()
except 子句可以在异常名称后指定一个变量,用于绑定一个异常实例,它的参数存储在 instance.args 中,通常出于方便考虑,异常实例会定义 __str__()
特殊方法,因此可以直接打印参数,而不需要用引用的形式.arg
,同时也可以在抛出之前首先实例化异常,并根据需要向其添加任何属性:
try:
raise Exception('spam', 'eggs')
except Exception as inst:
print(type(inst)) # the exception instance
print(inst.args) # arguments stored in .args
print(inst) # __str__ allows args to be printed directly,
# but may be overridden in exception subclasses
x, y = inst.args # unpack args
print('x =', x)
print('y =', y)
输出结果:
<class 'Exception'>
('spam', 'eggs')
('spam', 'eggs')
x = spam
y = eggs
有时遇到异常,我们不知道怎么处理,我们可以把异常抛出去,交给上面处理,Python 中 raise 语句允许程序员强制发生指定的异常,如:
>>> raise NameError('HiThere')
Traceback (most recent call last):
File "" , line 1, in <module>
NameError: HiThere
raise 语句唯一的参数就是要抛出的异常实例,这个实例尽量使用 Python 内置的异常类型,如果传递的是一个异常类,它将通过调用没有参数的构造函数来隐式实例化:raise ValueError # 等价于 'raise ValueError()'
raise 语句不带参数会把当前错误原样抛出。
此外,在 except 中 raise 一个 Exception,还可以把一种类型的异常转化为另一种类型,但尽量别这么干。
用户可以自定义异常,但是自定义异常时需要注意:
Exception
类派生。class Error(Exception):
"""Base class for exceptions in this module."""
pass
class InputError(Error):
"""Exception raised for errors in the input.
Attributes:
expression -- input expression in which the error occurred
message -- explanation of the error
"""
def __init__(self, expression, message):
self.expression = expression
self.message = message
class TransitionError(Error):
"""Raised when an operation attempts a state transition that's not
allowed.
Attributes:
previous -- state at beginning of transition
next -- attempted new state
message -- explanation of why the specific transition is not allowed
"""
def __init__(self, previous, next, message):
self.previous = previous
self.next = next
self.message = message
try 语句还有一个 finally 子句是可选的,用于处理在所有情况下都必须执行的清理操作。finally 子句将作为 try 语句的最后一项任务被执行,无论 try 子句是否发生异常,均会执行,如:
try:
print(a)
except NameError as e:
print('NameError: ', e)
finally:
print('finally...')
结果输出:
NameError: name 'a' is not defined
finally...
这里有一些 finally 的细节,自己以前没有注意到,看官方文档后发现挺重要的:
以上这些都是强调了 finally 子句一定会被执行,同时它的执行顺序优先于 try 语句无法处理的异常,也优先于 break、return、continue 等控制语句。
看两个官网给的例子:
>>> def bool_return():
... try:
... return True
... finally:
... return False
...
>>> bool_return()
False
一个更复杂的例子:
>>> def divide(x, y):
... try:
... result = x / y
... except ZeroDivisionError:
... print("division by zero!")
... else:
... print("result is", result)
... finally:
... print("executing finally clause")
...
>>> divide(2, 1) # try 子句没有异常,else子句执行,最后是finally
result is 2.0
executing finally clause
>>> divide(2, 0) # 异常被except 捕获,正确处理,finally 在其之后执行
division by zero!
executing finally clause
>>> divide("2", "1") # 这里except子句无法处理该类异常,所以先执行finally子句,再重新引发异常
executing finally clause
Traceback (most recent call last):
File "" , line 1, in <module>
File "" , line 3, in divide
TypeError: unsupported operand type(s) for /: 'str' and 'str'
在实际应用开发中,finally 子句非常有用,它常用于释放外部资源,如文件或者网络连接等。因为及时使用不成功,也可以成功释放这些资源。
最后列一下 try...except...else...finally
都出现的格式:
try:
codeblock
except:
codeblock
else:
codeblock
finally:
codeblock
读写文件是最常见的 IO 操作,在磁盘上读写文件的功能是由操作系统提供的,所以读写文件就是请求操作系统打开一个文件对象(file object),这通常描述为文件描述符,然后通过操作系统提供的接口从这个文件对象中读取数据,或者写入数数据。有点像,我们告诉操作系统我们要做什么,操作系统再去帮我们完成。
Python 中 open() 函数用来读写文件,用法和 C 是兼容的。它返回的是一个文件对象(file object)。open()函数有好几个参数,最常用的是 open(filename, mode=‘r’, encoding=None, errors=None)。
'r'
)。还有其他几种方式,待会列出来。encoding='utf-8'
。open() 函数默认是打开文本文件(text file),打开的方式主要有一些几种:
mode | 描述 |
---|---|
‘r’ | 默认情况,只读模式 |
‘w’ | 只写模式,注意存在的同名文件会被删除 |
‘a’ | 追加写模式,任何写入的数据都会自动添加到文件末尾 |
‘x’ | 只写模式,推荐,存在同名文件会提示报错 |
‘+’ | 上面的几种模式加上’+’,都变成可读可写,如:r+,w+,a+等 |
默认打开的是文本文件,如果我们要打开图片、视频等二进制文件时,就需要用二进制模式,模式和上面类似,只是加一个 ‘b’ 就可以了。
mode | 描述 |
---|---|
‘rb’ | 默认情况,只读模式 |
‘wb’ | 只写模式,注意存在的同名文件会被删除 |
‘ab’ | 追加写模式,任何写入的数据都会自动添加到文件末尾 |
‘xb’ | 只写模式,推荐,存在同名文件会提示报错 |
‘+’ | 上面的几种模式加上’+’,都变成可读可写,如:rb+,wb+,ab+等 |
举个栗子:
# 我在代码当前目录建了一个b.txt文件
f = open('b.txt', mode='r') # 打开文件
for line in f.readlines():
print(line, end='')
f.close() # 关闭文件
输出结果:
Java
Python
C
C++
如果文件 b.txt 不存在,会得到一个 FileNotFoundError 异常,文件对象的方法,我们待会讲。
注意,我们打开文件后,一定要记得关闭,不然文件对象会一直占用操作系统资源。所以这里使用 try…finally 来完成是非常好的。
try:
f = open('b.txt', mode='r')
for line in f.readlines():
print(line, end='')
finally:
if f:
f.close()
这样写很繁琐,Python 提供了更加优雅的方式帮助我们打开文件,会在我们使用结束文件,或者处理文件时发生异常,都能自动关闭文件,这也是 Python 官方推荐的方式。
with open('b.txt', mode='r') as f:
for line in f.readlines():
print(line, end='')
输出结果:
Java
Python
C
C++
发现 with 关键字的写法更加优雅,简洁,极其推荐。
接下来将简单介绍一些文件对象的方法。
Python 内置了很多文件对象的方法帮助我们对文件进行操作。下面列举一些常用的方法,更多的方法,大家可以使用 help(file object)进行查看,如 help(f),我们就可以看到我们可以对文件对象 f,进行哪些操作。
close()用于关闭文件。使用了 with 这个就可以忽略了。
read(self, size=-1, /),用于按大小读取文件的大小,如果不传入 size 这个仅限位置参数,则默认读取整个文件。
readline(self, size=-1, /),用于按行读取文件,每次读取文件的一行。
readlines(self, hint=-1, /),用于按行读取文件,hint 仅限位置参数用于指定要读取的行数,如果不指定,默认读取文件的全部行数,并放入一个列表中返回。
write(self, text, /),写入文本,并返回写入的文本字符数,即文本的长度。
writelines(self, lines, /),写入文本,不过是写入多行文本,需要我们传入一个列表。不过需要注意,每一行行末的换行符\n
需要我们自己添加。
seek(),指定文件指针在文件中的位置,seek(0) 代表将文件指针指向文件开头,seek(n)代表将文件指针指向第 n 个字符。
tell(),告诉我们当前文件指针的位置。
下面我们就测试一下:
我们先在当前目录新建一个 d.txt 文件里面内容如下:
C
Python
Java
测试如下:
>>> f = open('d.txt', mode='r+') # 读写方式打开d.txt
>>> f.tell() # 告诉我当前文件指针位置在0,即开头
0
>>> f.readline() # 读取返回文件的一行
'C\n'
>>> f.readline() # 读取返回文件的下一行
'Python\n'
>>> f.tell() # 告诉我当前文件指针位置在11,即读到了11个字符位置
11
>>> f.seek(0) # 将文件指针移到开头
0
>>> f.tell() # 可以看到文件指针回到了开头位置
0
>>> f.readlines(2) # readlines()读取两行,返回一个列表
['C\n', 'Python\n']
>>> f.readlines() # 读取剩余文件的全部行,返回一个列表
['Java']
>>> f.readline() # 文件读完了,继续读会得到一个空字符串
''
>>> f.tell() # 文件指针在 15
15
>>> f.seek(0) # 文件指针置于文件开始处
0
>>> f.read(11) # read(11)指定读取文件11个字符
'C\nPython\nJa'
>>> f.read() # 未指定参数,读取文件剩余的全部内容
'va'
>>> f.seek(0) # 将文件指针移到开头
0
>>> f.read() # 一次性读取全部文件内容到内存
'C\nPython\nJava'
>>> f.close() # 关闭文件
好了,看明白上面的,我们再简单看一个例子:
lines = ['C\n', 'Python\n', 'Java\n']
with open('d.txt', mode='w+') as f:
f.writelines(lines) # 写入多行文件
f.seek(0) # 文件指针置于开头,从头开始读取
for line in f: # 一行一行输出文件
print(line, end='')
结果输出:
C
Python
Java
理解每一个方法,恰当的使用它们,可以让我们的代码更加高效。
文件比较小,我们内存够大,所以方法选择上没那么重要,但是如果我们的文件非常大, 比如 8 个 G,这时如果你用 f.read()去读取,或者用 f.readlines(),默认读取整个文件,那么很可能你会因为内存不足,系统崩溃。
所以怎么读取大文件呢?
其实解决思路很简单,就是我们不要一次性都读入,把文件拆分,比如一行一行读入,或者一次读入 1024 个字符。
方法一(官网推荐):
with open('d.txt') as f:
for line in f:
print(line, end='')
输出结果:
C
Python
Java
这种情况适合一行一行读入,但是如果一行很大,通常视频图片时,比如一行有 1G 呢,这时候可以指定大小读入:
with open('d.txt') as f:
while True:
part = f.read(4) # 每次读取4个字符
if part:
print(part, end='')
else:
break
输出结果:
C
Python
Java
这里我们可以封装成一个函数,方便我们更加灵活的调用和配置:
def read_part(file_path, size=1024, encoding="utf-8"):
with open(file_path, mode='r', encoding=encoding) as f:
while True:
part = f.read(size)
if part:
yield part
else:
return None
file_path = r'd.txt' # r 代表原始字符串
size = 2 # 每次读取指定大小的内容到内存
encoding = 'utf-8'
for part in read_part(file_path=file_path, size=size, encoding=encoding):
print(part, end='')
按大小读取可以灵活控制一次读取的 size,在速度上较按行读取有优势,适用于一些大的二进制文件,比如读取一些大的视频或者图片等。
按行读取在处理一些文本的时候感觉更加便利,按行读更容易对文本进行处理。
当然我们也可以每次读入指定的行数,这里就不实现了。
到这里,我们基本把 Python 的基础部分总结完了。
推荐阅读: