本文將主要介紹 Python 的語法錯誤、異常、文件的讀取等基礎知識。閱讀本文預計需要 15 min
一文了解Python錯誤、異常和文件讀寫
1. 前言
錯誤和異常,以及讀取文件,寫入文件都是我們經常會遇到的。本文主要內容:
- 語法錯誤
- 異常定義和查看報錯信息
- 異常的處理
- 拋出異常
- 自定義異常
- 文件的讀取
- 讀取大文件的方法
2. 語法錯誤
在 Python 中主要分爲兩種錯誤:語法錯誤(syntax errors) 和異常(exceptions)。
語法錯誤,又稱解析錯誤(parsing errors),指代碼的語法不符合 Python 語法規則,導致“翻譯官” Python 解釋器無法翻譯你的代碼給計算機,導致會報錯。這很常見,尤其是我們初學編程的時候。
語法錯誤是代碼執行前檢測到的錯誤
。
舉個栗子:
>>> while True print('Hello world')
File "<stdin>", line 1
while True print('Hello world')
^
SyntaxError: invalid syntax
上面就是一個語法錯誤,學習如何看報錯信息。
首先我們看最後一行,SyntaxError: invalid syntax
這句就是告訴我們寫的代碼出現了 SyntaxError
即語法錯誤。
繼續往上面看,發現 Python 解釋器輸出了出現語法錯誤的一行,並且在這句代碼有問題的地方(也有可能是這附近,只是在這裏檢測出來了)用一個箭頭 ^
指示,同時再上面一行輸出了文件名 "<stdin>"
和出問題代碼所在的行數(line),這裏是第一行代碼出問題了,而且是在 print()函數附近。
通過這樣的順序,我們就可以快速的定位到錯誤代碼的位置,並且知道是什麼錯誤,從而進行修改。這句代碼主要是因爲 print()函數前面少了一個冒號(:
)。
學會看報錯信息,定位錯誤,對於調試代碼非常重要!語法錯誤相對比較簡單,就做這些總結。
3. 異常
除了語法錯誤,我們更多遇見的是異常(exception)。
3.1 異常的定義
有時候,我們的語法沒有任何問題,但是在代碼執行的時候,還是可能發生錯誤。這種在代碼運行時檢測到的錯誤成爲異常
。
下面展示一些常見的異常(來源於 Python 官網):
>>> 10 * (1/0)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero
>>> 4 + spam*3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'spam' is not defined
>>> '2' + 2
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Can't convert 'int' object to str implicitly
我們對比一下語法錯誤和異常的報錯信息,以 10 * (1/0)
的報錯信息爲例:
- 異常的最後一行也是告訴我們程序執行過程中遇到了什麼錯誤,異常的類型是什麼,這裏是
ZeroDivisionError
即分母爲 0 的異常。 - 我們在看第一行
Traceback (most recent call last):
這句是告訴我們程序在執行過程中追蹤到一個錯誤,下面開始一步一步追蹤定位錯誤。這在語法錯誤裏面是沒有的。因爲語法錯誤是在程序執行前檢測到的。官網的說法是以堆棧回溯的形式顯示發生異常時的上下文,通常它包含列出源代碼行的堆棧回溯,但是不會顯示從標準輸入中讀取的行。 - 接着一行
File "<stdin>", line 1, in <module>
是告訴我們,文件"<stdin>"
模塊中的第 1 行代碼出問題了。從而我們就定位到了代碼出錯的位置。可以發現,這裏沒有出現語法錯誤的箭頭。
有時候 Traceback 很長,這時候我們可以先看最後一行,知道錯誤類型,然後從上往下看報錯信息,最終定位到出問題代碼的位置(在哪個文件,多少行代碼),從而修改代碼。
異常的類型很多,這裏依次列出了 ZeroDivisionError, NameError 和 TypeError
三種異常,這些異常名稱是內置的標識符(identifiers),不是關鍵字(keywords)。
更多的內置異常(Built-in Exceptions)可以參看官網Built-in Exceptions獲取它們的介紹和意義。
3.2 異常的處理
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 語句的工作原理:
- 首先執行 try 子句(即 try 和 except 之間的所有語句).
- 如果沒有異常發生,則跳過 except 子句,從而結束 try 語句的執行。
- 如果執行 try 語句時發生了異常,則跳過 try 子句剩餘的部分。然後,如果異常的類型和 except 關鍵字後面的異常匹配,則執行相應的 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...")
... 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 "<stdin>", 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 "<stdin>", 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
3.3 拋出異常
有時遇到異常,我們不知道怎麼處理,我們可以把異常拋出去,交給上面處理,Python 中 raise 語句允許程序員強制發生指定的異常,如:
>>> raise NameError('HiThere')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: HiThere
raise 語句唯一的參數就是要拋出的異常實例,這個實例儘量使用 Python 內置的異常類型,如果傳遞的是一個異常類,它將通過調用沒有參數的構造函數來隱式實例化:raise ValueError # 等價於 'raise ValueError()'
raise 語句不帶參數會把當前錯誤原樣拋出。
此外,在 except 中 raise 一個 Exception,還可以把一種類型的異常轉化爲另一種類型,但儘量別這麼幹。
3.4 用戶自定義異常
用戶可以自定義異常,但是自定義異常時需要注意:
- 自定義的異常通常應該直接或間接從
Exception
類派生。 - 自定義的異常通常保持簡單,只提供許多屬性,這些屬性允許處理程序爲異常提取有關錯誤的信息。
- 在創建可能引發多個不同錯誤的模塊時,通常的做法是爲該模塊定義的異常創建基類,併爲不同的錯誤創建特定異常類的子類。如:
- 大多數異常名字都以 Error 結尾,類似於標準異常的命名。
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
3.5 定義清理操作
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 的細節,自己以前沒有注意到,看官方文檔後發現挺重要的:
- 如果在執行 try 子句期間發生了異常,該異常可由一個 except 子句進行捕獲處理。 如果異常沒有被某個 except 子句所處理,則該異常會在 finally 子句執行之後被重新引發。
- 異常也可能在 except 或 else 子句執行期間發生。 同樣地,該異常會在 finally 子句執行之後被重新引發。
- 如果在執行 try 語句時遇到一個 break, continue 或 return 語句,則 finally 子句將在執行 break, continue 或 return 語句之前被執行。
- 如果 finally 子句中包含一個 return 語句,則返回值將來自 finally 子句的某個 return 語句的返回值,而非來自 try 子句的 return 語句的返回值。
以上這些都是強調了 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 "<stdin>", line 1, in <module>
File "<stdin>", line 3, in divide
TypeError: unsupported operand type(s) for /: 'str' and 'str'
在實際應用開發中,finally 子句非常有用,它常用於釋放外部資源,如文件或者網絡連接等。因爲及時使用不成功,也可以成功釋放這些資源。
3.5 小結
最後列一下 try...except...else...finally
都出現的格式:
try:
codeblock
except:
codeblock
else:
codeblock
finally:
codeblock
- try 語句可以很好地處理一些異常,但是不要濫用,只在關鍵的位置使用。這個需要多積累經驗,看看牛人都是怎麼用的。
- except 子句可以有多個,但要注意父類和子類異常類型的順序。
- 不要輕易使用 except 後不加異常名稱,這會導致異常的類型無法確定,無法更好的定位異常。
- 要額外添加到 try 子句的代碼最好放到 else 子句中,else 子句是可選的,要使用 else,則 except 子句必須要有。這個需要再看看牛人怎麼用,借鑑用法。
- finally 子句是可選的,無論如何都會執行,但是要注意如果出現了 try 語句無法處理的異常時,會先執行 finally 子句,再重新引發異常。
- 如果在執行 try 語句時遇到一個 break, continue 或 return 語句,則先執行 finally 子句,再執行 break, continue 或 return 語句。
- 如果 finally 子句中包含一個 return 語句,則返回值將來自 finally 子句的某個 return 語句的返回值,而非來自 try 子句的 return 語句的返回值。
- finally 子句在使用文件、網絡連接等資源時非常有用,可以保證我們成功釋放資源。
4. 文件
讀寫文件是最常見的 IO 操作,在磁盤上讀寫文件的功能是由操作系統提供的,所以讀寫文件就是請求操作系統打開一個文件對象(file object),這通常描述爲文件描述符,然後通過操作系統提供的接口從這個文件對象中讀取數據,或者寫入數數據。有點像,我們告訴操作系統我們要做什麼,操作系統再去幫我們完成。
4.1 文件的讀寫
Python 中 open() 函數用來讀寫文件,用法和 C 是兼容的。它返回的是一個文件對象(file object)。open()函數有好幾個參數,最常用的是 open(filename, mode=‘r’, encoding=None, errors=None)。
- filename 代表文件的名字,需要我們傳入一個包含文件名的字符串,這是必傳參數。
- mode 也是需要我們傳入一個字符串,告訴函數,以什麼樣的方式打開文件,默認情況下是文件只能讀取(
'r'
)。還有其他幾種方式,待會列出來。 - encoding 是使用文件時的編碼或解碼的格式,這隻用於文本文件,不能用於二進制文件,默認模式和平臺有關,因此有時讀文本文件,我們會指定
encoding='utf-8'
。 - errors 是用於編碼錯誤的,如果一個文件存在多種編碼,這個時候,我們可以指定 errors=‘ignor’ 來忽略錯誤,這會造成部分數據丟失。
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 關鍵字的寫法更加優雅,簡潔,極其推薦。
接下來將簡單介紹一些文件對象的方法。
4.2 文件對象的方法
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
理解每一個方法,恰當的使用它們,可以讓我們的代碼更加高效。
4.3 大文件的讀取
文件比較小,我們內存夠大,所以方法選擇上沒那麼重要,但是如果我們的文件非常大, 比如 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 的基礎部分總結完了。
5. 巨人的肩膀
推薦閱讀: