Python之錯誤異常和檔案處理

RunTitan發表於2019-03-04
  • 之前的文章介紹的都是Python的一些語法和使用方法, 詳情可參考Python基礎知識
  • 然而這裡我們要說的是程式設計中我們最不想見到的, 但是卻也是不得不面對的Bug
  • 除此之外, 這裡還會介紹一下Python中的檔案讀取

錯誤和異常

Python 中(至少)有兩種錯誤:語法錯誤和異常(syntax errorsexceptions

語法錯誤

語法錯誤,也被稱作解析錯誤, 使我們在學習Python過程中最常遇到的錯誤, 來看看下面兩個錯誤示例:

if True
    print(`titan`)

# 錯誤資訊:
File "../5-讀檔案.py", line 19
    if True
          ^
SyntaxError: invalid syntax
複製程式碼
  • 語法分析器指出錯誤出現的檔案(File)和錯誤行(line 19)
  • 在檢測到錯誤的位置前面顯示一個小“箭頭”
  • 錯誤是由箭頭前面的標記引起的(或者至少是這麼檢測的)
  • 此處錯誤是因為Print函式的前面, if語句後面少了一個冒號(:)

異常

  • 在沒有語法錯誤的情況下, 當我們執行當前程式的時候也可能會引發錯誤
  • 執行期檢測到的錯誤稱為 異常,並且程式不會無條件的崩潰
  • 異常能夠編譯通過, 但是不能執行成功; 而語法錯誤不能編譯成功
print(1 / 0)
# 錯誤資訊:
File "../5-讀檔案.py", line 22, in <module>
    print(1 / 0)
ZeroDivisionError: division by zero


print(1 + "12")
# 錯誤資訊:
File "../5-讀檔案.py", line 22, in <module>
    print(1 + "12")
TypeError: unsupported operand type(s) for +: `int` and `str`


print(1 + ad * 2)
# 錯誤資訊:
File "../5-讀檔案.py", line 22, in <module>
    print(1 + ad * 2)
NameError: name `ad` is not defined
複製程式碼
  • 錯誤資訊的第一行, 指出了異常出現的檔案和錯誤行
  • 第二行, 提示了是哪一條語句出現了錯誤
  • 第三行, 指出了是哪一種異常資訊;異常也有不同的型別,異常型別做為錯誤資訊的一部分顯示出來
  • 以上三種異常分別為: 零除錯誤(ZeroDivisionError), 型別錯誤(TypeError) 和 命名錯誤(NameError)
  • 相關異常資訊官方文件

異常處理

  • 我們都知道, 正常情況下, 程式執行過程中遇到錯誤或者異常, 程式便會中斷執行, 這也就以為著後面的程式將無法執行
  • 但是在Python中, 我們可以針對異常做出一些處理, 使之在遇到異常錯誤時, 繼續執行後面的程式碼
  • 異常類其實是class類, 所有的錯誤都是繼承自BaseException

注意: 還有一些錯誤是無法跳過的, 比如記憶體錯誤

錯誤處理的語句

第一種格式

# 格式:

try:
    語句t
except 錯誤碼 as e:
    語句1
except 錯誤碼 as e:
    語句2
except 錯誤碼 as e:
    語句3
    ...
except 錯誤碼 as e:
    語句n
else:
    語句e
複製程式碼

需要注意的是:

  • 語句中的else是可有可無的
  • except語句中的as e也可以不加
# 錯誤處理的語句(else可有可無)
try.......except....else

# 格式:
try:
    語句t
except 錯誤碼1:
    語句1
except 錯誤碼2:
    語句2
else:
    語句3
複製程式碼

第二種格式

一個 except 子句可以在括號中列出多個異常的名字, 對於指定的一些異常做統一處理

try:
    print(7 / 0)
except (ZeroDivisionError, NameError):
    print(`程式異常`)
複製程式碼

第三種格式

無論遇到的是哪一種異常, 均做統一處理

try:
    print(7 / 0)
except :
    print(`程式異常`)
複製程式碼

try 語句工作方式

  • 首先,執行 try 子句 (在 tryexcept 關鍵字之間的部分)
  • 如果沒有異常發生, except 子句 在 try 語句執行完畢後就被忽略了
  • 如果在 try 子句執行過程中發生了異常,那麼該子句其餘的部分就會被忽略
  • 如果異常匹配於 except 關鍵字後面指定的異常型別,就執行對應的except子句。然後繼續執行 try 語句之後的程式碼
  • 如果發生了一個異常,在 except 子句中沒有與之匹配的分支,它就會傳遞到上一級 try 語句中
  • 如果最終仍找不到對應的處理語句,它就成為一個 未處理異常,終止程式執行,顯示提示資訊

使用示例:

一個 try 語句可能包含多個 except 子句,分別指定處理不同的異常。至多隻會有一個分支被執行。異常處理程式只會處理對應的 try 子句中發生的異常,在同一個 try 語句中,其他子句中發生的異常則不作處理

try:
    print(7 / 0)
except ZeroDivisionError:
    print(`除數為0`)
except NameError:
    print(`沒有改變數`)
except SyntaxError:
    print(`不知道`)
複製程式碼

一個 except 子句可以在括號中列出多個異常的名字

try:
    print(7 / 0)
except (ZeroDivisionError, NameError):
    print(`程式異常`)
複製程式碼

最後一個 except 子句可以省略異常名稱,以作為萬用字元使用。你需要慎用此法,因為它會輕易隱藏一個實際的程式錯誤!可以使用這種方法列印一條錯誤資訊,然後重新丟擲異常(允許呼叫者處理這個異常):

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子句,該子句只能出現在所有 except 子句之後。當 try 語句沒有丟擲異常時,需要執行一些程式碼,可以使用這個子句。例如:

try:
    print(7 / 0)
except ZeroDivisionError:
    print(`除數為0`)
except NameError:
    print(`沒有改變數`)
else:
    print(`程式碼OK`)
複製程式碼

丟擲異常

raise語句允許程式設計師強制丟擲一個指定的異常

raise NameError(`TitanJun`)

# 異常資訊:
Traceback (most recent call last):
  File "../1-異常處理.py", line 95, in <module>
    raise NameError(`TitanJun`)
NameError: TitanJun
複製程式碼

如果你需要明確一個異常是否丟擲,但不想處理它,raise 語句可以讓你很簡單的重新丟擲該異常:

try:
    raise NameError(`TitanJun`)
except NameError:
    print(`NameError錯誤`)
    raise

# 錯誤資訊:
NameError錯誤
Traceback (most recent call last):
  File "../1-異常處理.py", line 97, in <module>
    raise NameError(`TitanJun`)
NameError: TitanJun
複製程式碼

定義清理行為

  • try 語句還有另一個可選的子句: try--except--finally,目的在於定義在任何情況下都一定要執行的功能
  • 不管有沒有發生異常,finally子句 在程式離開 try 後都一定會被執行
  • try 語句中發生了未被 except 捕獲的異常(或者它發生在 exceptelse 子句中),在 finally 子句執行完後它會被重新丟擲。 – try語句經由 breakcontinuereturn 語句退 出也一樣會執行 finally 子句
try:
    print(7 / 0)
except ZeroDivisionError:
    print(`除數為0`)
except NameError:
    print(`沒有改變數`)
finally:
    print(`我一定要執行`)
    
# 輸出:
除數為0
我一定要執行
複製程式碼

預定義清理行為

有些物件定義了標準的清理行為,無論物件操作是否成功,不再需要該物件的時候就會起作用。以下示例嘗試開啟檔案並把內容列印到螢幕上

for line in open("myfile.txt"):
    print(line)
複製程式碼

這段程式碼的問題在於在程式碼執行完後沒有立即關閉開啟的檔案。這在簡單的指令碼里沒什麼,但是大型應用程式就會出問題。with 語句使得檔案之類的物件可以 確保總能及時準確地進行清理

with open("myfile.txt") as f:
    for line in f:
        print(line)
複製程式碼

語句執行後,檔案 f 總會被關閉,即使是在處理檔案中的資料時出錯也一樣。其它物件是否提供了預定義的清理行為要檢視它們的文件

檔案讀寫

  • Python中檔案的讀寫, 通常以文字開啟,這意味著,你從檔案讀出和向檔案寫入的字串會被特定的編碼方式(預設是UTF-8)編碼。
  • 模式後面的 `b` 以 二進位制模式 開啟檔案:資料會以位元組物件的形式讀出和寫入。
  • 這種模式應該用於所有不包含文字的檔案

讀取檔案

函式 open() 返回檔案物件,通常的用法需要兩個引數

def open(file, mode=`r`, buffering=None, encoding=None, errors=None, newline=None, closefd=True)

# 使用
path = r`/Users/xxx/text.txt`
file = open(path, `r`)
複製程式碼
  • 引數一: 一個含有檔名的字串
  • 引數二: 描述如何使用該檔案的字串, 預設為 `r`
    • `r`: 時表示只是讀取檔案
    • `rb`: 以二進位制形式開啟一個檔案用於只讀, 檔案描述放在檔案的開頭
    • `w`: 表示只是寫入檔案(已經存在的同名檔案將被刪掉)
    • `wb`: 開啟一個檔案用於寫入二進位制, 如果該檔案已經存在會覆蓋, 如果不存在則建立新檔案
    • `w+`: 開啟一個檔案用於讀寫
    • `a`: 表示開啟檔案進行追加,寫入到檔案中的任何資料將自動新增到末尾
    • `r+`: 表示開啟檔案進行讀取和寫入
    • `b`: 以 二進位制模式 開啟檔案

檔案物件方法

read()

  • 要讀取檔案內容,需要呼叫 file.read(size),該方法讀取若干數量的資料並以字串形式返回其內容
  • size 是可選的數值,指定字串長度, 如果沒有指定 size 或者指定為負數,就會讀取並返回整個檔案。
  • 當檔案大小為當前機器記憶體兩倍時,就會產生問題。反之,會盡可能按比較大的 size 讀取和返回資料。
  • 如果到了檔案末尾,file.read()會返回一個空字串
# 讀取檔案
str = file.read()
print(str)
複製程式碼

readline()

  • file.readline() 從檔案中讀取單獨一行,字串結尾會自動加上一個換行符(
    ),只有當檔案最後一行沒有以換行符結尾時,這一操作才會被忽略。
  • 這樣返回值就不會有混淆,如果 file.readline() 返回一個空字串,那就表示到達了檔案末尾,如果是一個空行,就會描述為 `
    `
    ,一個只包含換行符的字串
file.readline()
複製程式碼

遍歷檔案物件

可以迴圈遍歷檔案物件來讀取檔案中的每一行。這是一種記憶體高效、快速,並且程式碼簡介的方式

for line in file:
    print(line)
複製程式碼

如果你想把檔案中的所有行讀到一個列表中,你也可以使用 `list(file)` 或者 `file.readlines()`

# 把檔案讀到列表中
print(list(file))
print(file.readlines())
複製程式碼

寫入檔案

  • write: 將 string 的內容寫入檔案,並返回寫入字元的長度
  • writelines: 用於向檔案中寫入一序列的字串, 沒有返回值
# 寫入檔案
leng = file.write(`我是一隻小鴨子`)
print(leng)
# 輸出: 7


# 寫入一個序列
file.writelines([`hello`, `Python`])
複製程式碼

想要寫入其他非字串內容,首先要將它轉換為字串

tell/seek

  • tell: 返回一個整數,代表檔案物件在檔案中的指標位置,該數值計量了自檔案開頭到指標處的位元數。
  • 需要改變檔案物件指標話話,使用 file.seek(offset,from_what)
  • 指標在該操作中從指定的引用位置移動 offset 位元,引用位置由 from_what 引數指定。
  • from_what 值為 0 表示自檔案起始處開始,1 表示自當前檔案指標位置開始,2 表示自檔案末尾開始。
  • from_what 可以忽略,其預設值為零,此時從檔案頭開始
l = file.readline()
print(l)

pos = file.tell()
print(pos)

# 輸出:
b`https://www.titanjun.top/
`
26
複製程式碼

重新設定檔案讀取指標到開頭

file.seek(5, 0)
print(file.readline())

# 輸出:
b`https://www.titanjun.top/
`
b`://www.titanjun.top/
`
複製程式碼

close

  • 當你使用完一個檔案時,呼叫 file.close() 方法就可以關閉它並釋放其佔用的所有系統資源。
  • 在呼叫 file.close() 方法後,試圖再次使用檔案物件將會自動失敗
file.close()

file.read()

File "../5-讀檔案.py", line 64, in <module>
    file.read()
ValueError: read of closed file
複製程式碼

關鍵字with

  • 用關鍵字 with 處理檔案物件是個好習慣。
  • 它的先進之處在於檔案用完後會自動關閉,就算髮生異常也沒關係。
  • 它是 try-finally 塊的簡寫
with open(path, `rb+`) as file:
    str = file.read()
    print(str)
file.close()
複製程式碼

相關文章