程式出錯了怎麼辦？答案就在畫面裡--帶初學者看錯誤訊息
Tags: python, colab, jupyter
Published: 2022-03-12 10:53:47

剛學習 Python 程式設計的人常常會在執行出狀況時不知所措, 這主要是因為 Python 直譯器會噴出一大堆訊息, 但卻是英文的訊息, 雖然大家都學過英文, 可是程式語言裡的英文單字有些並不是一般英文常出現的, 而有些語句也像是法律條文為求精準簡潔, 並不是那麼簡明易懂, 本文就希望能帶大家習慣閱讀錯誤訊息, 通常只要看懂錯誤訊息, 解決方法就自然浮現了。

## 語法錯誤

初學者最常見的錯誤是**語法錯誤 (syntax error)**, 這指的是沒有依照 Python 的語法規則寫程式, Python 看不懂無法執行, 比較常見的是少了對應的引號或是括號, `for`、`while` 等複合敘述少了冒號等等。

### 互動環境下的語法錯訊息

我們先以在 Python 互動環境下少打了字串的結尾引號為例 (不同版本的 Python 顯示的訊息可能會不同, 本文以 3.10.2 為主要執行環境)：

```python
>>> print("hello)
  File "<stdin>", line 1
    print("hello)
          ^
SyntaxError: unterminated string literal (detected at line 1)
>>>
```

錯誤訊息內包含兩個部分：分別是發生錯誤的位置以及錯誤的類型與說明。我們先以剛剛的例子說明第一部份：

```
  File "<stdin>", line 1
    print("hello)
          ^
```

其中第 1 列會告訴你是在哪一個檔案裡的第幾列程式發生錯誤：

```
  File "<stdin>", line 1
       ---------  ------
           ^         ^
           |         |
      發生錯誤的檔案    -- 檔案裡的哪一列發生錯誤
```

當我們在 Python 互動環境下測試程式時, 檔案名稱會是 \<stdin\>, stdin 代表標準輸入設備, 就是你的鍵盤, 表示是透過鍵盤直接輸入程式讓 Python 互動環境執行。

接著第 2 列會顯示發生錯誤的那一列程式, 以及是在這一列的哪一個位置發生錯誤：

```
    print("hello)
          ^
```

你會看到程式的內容, 而且它會用 '^' 符號指出錯誤的位置, 像是本例就指出在字串開頭這邊出錯了。如果是簡單的錯誤, 看到這裡大概就知道問題, 不過看不出來也沒關係, 再看錯誤訊息的第二部分, 就可以知道錯誤的細節。

錯誤訊息的第二部分分成錯誤的類型與說明, 一樣以剛剛的例子來解釋：

```
SyntaxError: unterminated string literal (detected at line 1)
-----------  ------------------------------------------------
     ^                             ^
     |                             |
  錯誤類型                     錯誤的詳細說明
```

在本例中, 錯誤的類型是 **SyntaxError**, 直譯就是『語法錯誤』, 表示沒有依照 Python 的規定寫程式, 所以 Python 讀取程式內容後無法執行該列程式。

在錯誤的說明中, 它告訴我們遇到**未結尾 (unterminated)** 的字串, **literal** 這個字在程式設計上指的是直接以文字書寫出資料的內容, 像是在程式中直接寫明整數 23、浮點數 5.36、字串 "hello" 等等這些都叫做 literal。因此, 整個說明告訴我們的就是程式中書寫的字串沒有結尾, 由於 Python 規定書寫字串內容時必須以一對英文引號包起來, 對照錯誤訊息的第一部分指出的錯誤位置, 就可以發現是 hello 之後少打了對應結尾的英文雙引號, 只要補上就可以了。此外, 說明的最後還很貼心地再次告訴你這個錯誤是在程式的第一列**發現 (detected)** 的。

要特別說明的是, 錯誤訊息指出的錯誤位置未必就是少打字的地方, 但一定就是在附近, 只要對照錯誤的說明, 就可以找到真正錯誤的地方。

### 執行完整程式檔時的語法錯誤訊息

如果你是執行程式檔, 例如我們把剛剛的例子寫在 test\.py 檔中：

```python
❯ type test.py
print("hello)
```

然後在終端機中透過命令列執行：

```python
❯ py test.py
  File "D:\code\python\test.py", line 1
    print("hello)
          ^
SyntaxError: unterminated string literal (detected at line 1)

❯
```

就會看到一模一樣的錯誤訊息, 唯一的差別就是錯誤位置是以**實際的程式檔名**標示, 而不是代表鍵盤輸入的 "stdin"。

### 在 Visual Studio Code 等開發工具中執行程式的語法錯誤訊息

很多人是使用開發工具寫程式, 這裡就以 Visual Studio Code 為例, 我們開啟剛剛的 test\.py 檔執行, 就會在下半部開啟終端機窗格以命令列執行, 所以會看到一模一樣的錯誤訊息：

![VSCode 錯誤訊息](https://i.imgur.com/XrD1HgX.png)

其實在開發工具中撰寫程式的同時, 就已經會針對語法錯誤提出警告, 以 Visual Studio Code 安裝的 Pylance 延伸模組為例, 它會在偵測到語法錯誤的地方表示紅色的波浪線, 只要將滑鼠移到波浪線處, 就會出現提示訊息：

![Pylance 語法錯誤](https://i.imgur.com/oBgU92z.png)

像是上圖就告訴你這裡書寫的字串沒有結尾。

另外, Pylance 也會在下方的**問題**窗格顯示目前偵測到的所有語法錯誤：

![Pylane 語法檢查](https://i.imgur.com/RRaBqJJ.png)

像是上圖就顯示有兩個語法錯誤, 只要點選錯誤訊息, 編輯窗格中就會標示錯誤的地方。本例中第一個錯誤是：

```
"(" was not closed
```

表示 "(" 沒有**關閉 (closing)**, 意思就是沒有對應的 ")"。

第二個錯誤告訴我們書寫的字串沒有結尾：

```
String literal is unterminated
```

這個錯誤也導致了第一個錯誤, 因為字串沒有結尾, 所以原本對應 "(" 的 ")" 被當成字串的內容, "(" 就沒有對應的 ")" 了。

### 在 Colab 或是 Jupyter 中的語法錯誤訊息

如果你是在 Colab 中執行程式, 會看到如下的錯誤訊息：

![Colab 語法錯誤](https://i.imgur.com/R8IX3fw.png)

你可以看到錯誤訊息的格式和前面介紹的類似, 但是有兩點不同：

1. 在 Colab 中是以**儲存格**為單位, 所以在 'File' 後面是 Colab 幫這個儲存格編的**識別名稱**, 標示的列號也是該儲存格內的列號：

    ```
      File "<ipython-input-1-2fadf01e7f7a>", line 1
    ```
    
    識別名稱中 "ipython-input-" 後面的數字是儲存格的**執行序號**, 像是上例中就表示是在執行順序 1 號的儲存格出錯。

3. 由於我測試時 Colab 的 Python 版本為 3.7.12, 不同版本的 Python 顯示的錯誤訊息可能會有差異, 像是這裡標示的錯誤位置是在右括號之後：

    ```
        print("hello)
                     ^    
    ```
1. 錯誤類型雖然一樣, 但是錯誤訊息是『在掃視 (scanning) 書寫的字串內容時遇到一列的結尾 (EOL 是 End Of Line)』, 表示沒有看到字串結尾的引號這一列就結束了：
    ```
    SyntaxError: EOL while scanning string literal
    ```

    因此, 錯誤的位置是標示在這一列程式的結尾處。
    
1. Colab 還很貼內心的在錯誤訊息後提供 StackOverflow 連結的按鈕, 方便你到該網站尋求解答。

### Jupyter (IPython) 中的語法錯誤訊息

如果是在 Jupyter (IPython) 中執行程式, 看到的錯誤訊息格式和 Colab 很類似：

![Jupyter 語法錯誤](https://i.imgur.com/ayl3khT.png)

但是在標示錯誤位置時會直接以儲存格的執行序號代替 Colab 中比較冗長且看起來複雜的識別名稱：

```
  Input In [1]
    print("hello)
          ^
```

但是沒有儲存格內的列編號, 只能在錯誤說明結尾處看到列編號：

```
SyntaxError: unterminated string literal (detected at line 1)
```

由於這個 Jupyter 環境是以 Python 3.10.2 運行, 所以看到的錯誤訊息和互動環境下是一樣的。

## 執行時期錯誤 (runtime error)

對比於語法錯誤是還沒執行就發現程式寫法不合規定, 執行時期錯誤則是執行程式後才發生的錯誤。

### 互動環境下的執行時期錯誤訊息

我們以一個簡單的例子來說明執行時期錯誤：

```python
>>> a = 20
>>> print(a[2])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'int' object is not subscriptable
>>>
```

在這個例子裡, 因為我們把整數當成像是串列這樣的物件使用, 想要取得索引 2 的項目而出錯。錯誤訊息和語法錯誤時一樣, 分成錯誤位置與錯誤類型及說明兩部分, 我們先來看第二個部分：

```
TypeError: 'int' object is not subscriptable
```

它告訴我們這是**TypeError**, 也就是**型別錯誤**, 意思就是這裡不能用這種類型的資料, 後面的說明就明確解釋 int 物件不能**以索引取值 (subscriptable)**, 也就是不能搭配 `[]` 運算器使用。了解這些後, 只要找到出錯的那一列程式, 看到 `[]` 運算器, 就可以發現運算元 `a` 是整數, 不能使用 `[]` 運算器, 修正就可以解決問題。

在標示錯誤位置的地方, 除了原本標示檔案名稱與列編號以外, 一開始的地方多了一列開頭是 **"Traceback"** 的訊息, 它會帶你**往回追溯 (traceback)** 導致錯誤發生的執行過程：

```
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
```

不過這個例子只有一列程式, 看不出回溯歷程的意思, 我們改以底下的例子來說明：

```python
>>> def f(a):
...     return a[2]
...
>>> print(f(20))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in f
TypeError: 'int' object is not subscriptable
>>>
```

你會看到回溯歷程有兩列, 第一列如下：

```
  File "<stdin>", line 1, in <module>
```

這告訴我們在程式的第一列出錯了, 後面 **"in \<module\>"** 的意思是這一列程式並**不在任何的類別或函式**中, 而是在該程式檔的最外層。由於出錯的這一列程式是叫用 `f()` 函式, 錯誤是發生在該函式中, 因此要再看回溯歷程的下一列才會知道真正出錯的地方：

```
  File "<stdin>", line 2, in f
```

它告訴我們在 `f()` 函式的第二列發生了型別錯誤, 只要依據錯誤說明找出這一列哪裡出錯即可。

要注意的是回溯歷程是依照先後次序 (most recent call last) 排列, 像是剛剛的例子, 程式是先執行：

```python
>>> print(f(20))
```

然後才進到 `f()` 函式內的第 2 列：

```python
>>> def f(a):
...     return a[2]
...
```

所以在回溯歷程中也是依照這樣的順序排列。

### 完整程式檔的執行時期錯誤訊息

如果是執行完整的程式檔, 例如：

```python
❯ type test.py
def f(a):
    return a[2]

print(f(20))
```

看到的錯誤訊息如下：

```
❯ py test.py
Traceback (most recent call last):
  File "D:\code\python\test.py", line 4, in <module>
    print(f(20))
  File "D:\code\python\test.py", line 2, in f
    return a[2]
TypeError: 'int' object is not subscriptable
❯
```

跟在互動環境下看到的差別在於：

- 和語法錯誤一樣, 是以真正的檔案名稱標示。
- 程式的列號是以程式檔內的整體編號順序。
- 除了標示檔案名稱與列號, 也會把出錯的那一列程式列出來, 方便查看程式內容。

### 在 Colab 中的執行時期錯誤

如果是在 Colab 中執行一樣的程式, 看到的錯誤訊息如下：

![Colab 執行時期錯誤](https://i.imgur.com/JfPT8e9.png)

看到的訊息很類似, 但是會列出出錯處鄰近的程式供參考, 也會明確指出出錯的程式是哪一列：

```
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-5-516efd866f6a> in <module>()
      2     return a[2]
      3 
----> 4 print(f(20))

<ipython-input-5-516efd866f6a> in f(a)
      1 def f(a):
----> 2     return a[2]
      3 
      4 print(f(20))

TypeError: 'int' object is not subscriptable
```

如果實際出錯的地方是在不同的儲存格, 也可以藉由識別名稱中的執行序號找到出錯的儲存格, 例如若在新的儲存格叫用 `f()`：

![Colab 執行時期錯誤](https://i.imgur.com/Hm0uMaT.png)

你可以清楚的看到執行序號 6 的儲存格叫用了執行序號 5 的儲存格內的 `f()` 而出錯：

```
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-6-925a89c490fa> in <module>()
----> 1 print(f(200))

<ipython-input-5-516efd866f6a> in f(a)
      1 def f(a):
----> 2     return a[2]
      3 
      4 print(f(20))

TypeError: 'int' object is not subscriptable
```

### Jupyter 中的執行時期錯誤

Jupyter 的標示方式就跟 Colab 很類似, 不過和語法錯誤不同的是, 執行時期錯誤會顯示列號：

![Jupyter 執行時期錯誤](https://i.imgur.com/QP2lqu0.png)

它還非常貼心的進一步以不同顏色標示出在出錯的那一列程式裡出錯的地方。

如果出錯的地方在不同的儲存格, 標示的方式也是一樣：

![Jupyter 執行時期錯誤](https://i.imgur.com/DrZQqlW.png)

可以看出來是執行序號 4 的儲存格叫用了執行序號 2 的儲存格內的 `f()` 而出錯。

### 叫用其他模組時出錯

有了以上的基礎, 現在遇到錯誤訊息就可以冷靜對待, 例如使用 Python 提供的模組卻出錯：

```python
>>> import os.path
>>> os.path.getatime("xxx.txt")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "D:\Program Files\Python310\lib\genericpath.py", line 60, in getatime
    return os.stat(filename).st_atime
FileNotFoundError: [WinError 2] 系統找不到指定的檔案。: 'xxx.txt'
>>>
```

這裡我們想要取得特定檔案最後的存取時間, 從回溯歷程可以知道實際上是在叫用 `os.stat()` 時出錯, 不過在 Windows 上這個錯誤的訊息是中文的, 所以一看就懂。相同的程式在 Linux 上是英文：

```python
>>> import os.path
>>> os.path.getatime("xxx.txt")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3.9/genericpath.py", line 60, in getatime
    return os.stat(filename).st_atime
FileNotFoundError: [Errno 2] No such file or directory: 'xxx.txt'
>>>
```

不過只要認真讀一下, 就知道問題出在指定的檔案並不存在。閱讀錯誤訊息還有一個好處, 就是可以知道實作的細節, 像是在這個例子哩, 我們可以知道 `os.path.getatime()` 是透過 `os.stat()` 達成, 如果想知道更進一步的細節, 我們也知道原始檔在哪裡, 只要找到 genericpath\.py 內的第 60 列, 就可以看到實作內容：

```python
    57
    58  def getatime(filename):
    59      """Return the last access time of a file,reported by os.stat()."""
    60      return os.stat(filename).st_atime
    61
```

出錯還可以學東西, 是不是很賺？

## 小結

看到這裡, 我們就具備了基本功, 剩下的就是看到各種錯誤訊息時, 能不能看懂其中的關鍵詞彙？例如底下的錯誤：

```python
>>> for i in 20:
...     print(i)
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'int' object is not iterable
>>>
```

錯誤訊息說 `int` 物件不是 `iterable`, 那麼什麼是 `iterable` 嗎？我們不可能在這篇文章中把所有的錯誤訊息都講解一遍, 得靠自己查閱文件, 這樣的基本功還是不可避免的。