跳至主要内容

Python 中的异常与错误处理

错误和异常可能导致程序失败或意外行为,Python 自带一套强大的工具来提升代码稳定性。
· 11分钟 阅读

使用 AI 探索

错误和异常可能导致意外行为,甚至让程序停止执行。Python 提供了多种函数和机制来处理这些问题,从而提高代码的健壮性。本文将通过示例学习各种错误类型和内置函数。

  • 错误(error)是代码中的严重问题,通常无法在运行时恢复,例如语法格式错误或缩进错误。
  • 异常(exception)是在运行时发生的事件,会打断程序的正常流程,但可以被捕获并处理,使程序得以继续。

虽然异常发生在运行时,但某些错误(如 SyntaxError 和 IndentationError)会在程序运行前被解析器捕获。

简单来说,错误是正常应用不应捕获的致命问题,而异常是程序应该捕获并处理的情况。

Python 中 try except else finally 的流程

接下来通过各种示例进一步了解错误与异常。为便于运行这些示例,您可以免费创建一个 DataLab 工作簿,其中预装了 Python 并包含所有代码样例。

Python 中的错误

下面是一个语法错误示例:函数外部的 return 没有意义。我们不应在程序中处理这类错误,而是要正确定义一个返回字符串的函数。

return "DataCamp"
Input In [1]
    return "DataCamp"
    ^
SyntaxError: 'return' outside function

我们定义了函数,但缩进不正确。缩进错误不应在运行时处理,应该手动修复或使用代码格式化工具。

def fun():
return "DataCamp"    
Input In [2]
    return "DataCamp"
    ^
IndentationError: expected an indented block

Python 中的异常

我们遇到了一个 ZeroDivisionError(异常)。可以在运行时使用 try 和 except 块来处理。

test = 1/0
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
Input In [4], in <cell line: 1>()
----> 1 test = 1/0

ZeroDivisionError: division by zero

NameError 异常很常见,通常是未找到变量。我们可以通过替换变量或打印警告来处理该异常。

y = test
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Input In [5], in <cell line: 1>()
----> 1 y = test

NameError

Python 内置异常

以下是 Python 默认异常及其说明:

  1. AssertionError:当 assert 语句失败时引发。

  2. EOFError:当 input() 函数遇到文件结束条件时引发。

  3. AttributeError:当属性赋值或引用失败时引发。

  4. TabError:当缩进由不一致的制表符或空格组成时引发。

  5. ImportError:当导入模块失败时引发。

  6. IndexError:当序列的索引超出范围时发生。

  7. KeyboardInterrupt:当用户输入中断键(Ctrl + C 或 Delete)时引发。

  8. RuntimeError:当错误不属于任何特定类别时发生。

  9. NameError:当在局部或全局作用域中找不到变量时引发。

  10. MemoryError:当程序内存耗尽时引发。

  11. ValueError:当操作或函数接收到类型正确但值不正确的参数时发生。

  12. ZeroDivisionError:当用零去除某个值或变量时引发。

  13. SyntaxError:当 Python 语法错误时由解析器引发。

  14. IndentationError:当缩进不正确时发生。

  15. SystemError:当解释器检测到内部错误时引发。

您可以在文档中找到完整的错误与异常列表。

通过我们的 Object-Oriented Programming in Python 课程学习 Python 异常。课程将教您如何创建类,并利用继承与多态来复用与优化代码。

使用 try、except、else 与 finally 进行异常处理

了解错误与异常之后,我们将通过 try、except、else 和 finally 块来学习如何处理它们。

所谓“处理”,是什么意思?在常规情况下,这些错误会停止代码执行并显示错误信息。为了构建稳定的系统,我们需要预判这些错误,并提供替代方案或警告信息。

本节将学习每个代码块的作用,以及如何用它们来编写健壮的代码。

The try and except statement 

在 Python 中处理异常最简单的方式是使用 try 和 except 块。

  1. 运行 try 语句下的代码。

  2. 当引发异常时,执行 except 语句下的代码。

这样,代码不会在错误或异常处停止,而是转入替代方案。

Python 中的 try except 语句

简单示例

第一个示例中,我们尝试打印未定义的变量 x。在常规情况下,这会抛出错误并停止执行,但借助 try 和 except 块,我们可以改变控制流。

  • 程序先运行 try 语句下的代码。
  • 由于 x 未定义,所以会运行 except 语句并打印警告。
try:
   print(x)
except:
   print("An exception has occurred!")
An exception has occurred!

多个 except 语句示例

第二个示例中,我们使用多个 except 语句来处理多种异常类型。

  • 如果引发 ZeroDivisionError,程序将打印 “You cannot divide a value with zero.”
  • 其余异常将打印 “Something else went wrong.”

这样就可以编写在不崩溃的情况下同时处理多种异常的灵活代码。

注意:避免使用不带异常类型的裸 except: 子句——它会捕获所有内容,包括 KeyboardInterrupt 和 SystemExit,从而可能导致程序无法退出并隐藏无关的缺陷。务必只捕获您预期的特定异常,例如 except ZeroDivisionError:。

try:
   print(1/0)
        except ZeroDivisionError:
   print("You cannot divide a value with zero")
except:
   print("Something else went wrong")
You cannot divide a value with zero

加载文件示例

现在来看一个更实际的例子。

在下面的代码中,我们尝试读取 CSV 文件。当引发 FileNotFoundError 异常时,代码会打印该错误以及关于 data.csv 的附加提示。

没错,我们可以在不中断执行的情况下打印默认错误消息。

try:
   with open('data.csv') as file:
       read_data = file.read()
except FileNotFoundError as fnf_error:
   print(fnf_error)
   print("Explanation: We cannot load the 'data.csv' file")
[Errno 2] No such file or directory: 'data.csv'
Explanation: We cannot load the 'data.csv' file

The try with else clause

我们已经学习了 try 和 except,接下来学习 else 语句。

当 try 语句未引发异常时,代码会进入 else 块。它是一种补救或回退选项,适用于您预期脚本某部分可能会产生异常的情况。通常用于简短的准备或校验环节,以避免掩盖某些错误。

注意:在 try-except 结构中,else 需要放在所有 except 语句之后。

Python 中带 else 的 try

简单示例

我们在 ZeroDivisionError 示例中加入 else。如您所见,当没有异常时,会执行 else 下的 print 函数并显示结果。

try:
   result = 1/3
except ZeroDivisionError as err:
   print(err)
else:
   print(f"Your answer is {result}")
Your answer is 0.3333333333333333

带 else 的 IndexError 示例

我们通过创建一个简单函数并在多种场景下测试来进一步学习。

find_nth_value() 函数以 x(列表)和 n(索引编号)为参数。我们使用了 try、except 和 else 来处理 IndexError 异常。

x = [5,8,9,13]

def find_nth_value(x,n):
    try:
        result = x[n]
    except IndexError as err:
        print(err)
    else:
        print("Your answer is ", result)

列表 x 含有四个值,我们将分别测试第 6 和第 2 个索引。

# Testing
find_nth_value(x,6)
find_nth_value(x,2)
  • 当 n=6 时,引发 IndexError,我们看到了默认错误消息 “list index out of range”。
  • 当 n=2 时,没有引发异常,函数打印了 else 语句下的结果。
list index out of range
Your answer is  9

Python 中的 finally 关键字

在 try-except 结构中,无论是否有异常,finally 关键字总会被执行。finally 块总是在 try、except 和 else 之后执行,不受是否引发或捕获异常的影响。它非常适合用于资源清理和对象关闭,尤其是关闭文件。

Python 中的 finally 关键字

divide 函数用于处理 ZeroDivisionError 异常,并在没有异常时显示结果。无论结果如何,都会运行 finally,以绿色打印 “Code by DataCamp”。

def divide(x,y):
    try:
        result = x/y
    except ZeroDivisionError:
        print("Please change 'y' argument to non-zero value")
    except:
        print("Something went wrong")
    else:
        print(f"Your answer is {result}")
    finally:
        print("\033[92m Code by DataCamp\033[00m")

第一次测试中,我们用 1 除以 0,会引发 ZeroDivisionError 并打印消息。可以看到,错误信息之后还有一行输出。

divide(1,0)
Please change 'y' argument to non-zero value
 Code by DataCamp

当输入有效时,会执行 else 和 finally 并显示结果。

divide(3,4)
Your answer is 0.75
 Code by DataCamp

如果第二个参数传入字符串而不是整数,会引发不同于 ZeroDivisionError 的异常,并显示不同的信息。

divide(1,'g')
Something went wrong
 Code by DataCamp

这三种场景有一个共同点:代码总会运行 finally 下的打印语句。

如果您是 Python 新手,并希望像专业程序员一样编码,可以尝试我们的 Python Programming 技能路径。您将学习高效编码、Python 函数、软件工程、单元测试与面向对象编程。

Python 中的嵌套异常处理

当我们希望程序按顺序处理多类异常时,需要使用嵌套异常处理。例如,可以在 else 语句下再添加一个 try-except 块。如果第一段代码未引发异常,就接着检查第二段代码。

修改 divide 函数

我们在前面的 divide 函数基础上修改,在 else 语句下添加了一个嵌套的 try-except 块。这样,如果没有 AttributeError,就进入 else,并检查新代码是否会引发 ZeroDivisionError。

def divide(x,y):
    
    try:
        value = 50
        x.append(value)
    
    except AttributeError as atr_err:
        print(atr_err)
        
    else:
        try:
            result = [i / y for i in x]
            print( result )
 except ZeroDivisionError:
            print("Please change 'y' argument to non-zero value")

    finally:
        print("\033[92m Code by DataCamp\033[00m")

第一种场景中,我们提供含四个值的列表 x 和分母 3。脚本会向列表添加 50,将列表中的每个值除以 3,并显示结果。

x = [40,65,70,87]
divide(x,3)

函数成功执行,未引发任何异常。

[13.333333333333334, 21.666666666666668, 23.333333333333332, 29.0, 16.666666666666668]
 Code by DataCamp

如果第一个参数传入整数而不是列表,会引发 AttributeError。

divide(4,3)
'int' object has no attribute 'append'
 Code by DataCamp

最后一种场景中,传入列表,但第二个参数为 0,在 else 语句下引发 ZeroDivisionError。

divide(x,0)
Please change 'y' argument to non-zero value
 Code by DataCamp

文件编辑示例

我们来看更贴近实际的例子:加载文件、写入文本并关闭文件。

file_editor() 函数将:

  • 检查 open() 函数是否引发 FileNotFoundError。
  • 如果外层未引发异常,则检查 write() 函数的异常。
  • 无论如何,在打开文件后都会通过 finally 语句关闭文件。
  • 如果外层 try 语句引发异常,将返回带有无效文件路径的错误消息。
def file_editor(path,text):
    try:
      data = open(path)

      try:
        data.write(text)

      except:
        print("Unable to write the data. Please add an append: 'a' or write: 'w' parameter to the open() function.")

      finally:
        data.close()

    except:
      print(f"{path} file is not found!!")

第一种场景中,我们提供文件路径与文本。

path = "data.txt"
text = "DataLab: Share your data analysis in a cloud-based environment--no installation required."

file_editor(path,text)

外层异常被引发。

data.txt file is not found!!

要解决“找不到文件”的异常,我们需要使用 Linux 的 echo 命令创建 data.txt 文件。

!echo "File by DataCamp" > "data.txt"

Workspace 中的文件编辑

之后,重新运行 file_editor() 函数。

file_editor(path,text)

此次引发了内层异常,因为 write() 函数无法写入文本。

Unable to write the data. Please add an append: 'a' or write: 'w' parameter to the open() function.

要解决该问题,需要将第三行从 data = open(path) 改为 data = open(path, 'a'),这样即可向文件追加新文本。

再次运行函数后,文本已成功写入文件。

file_editor(path,text)

Workspace 中文件编辑截图

并不推荐使用嵌套异常处理,因为它会增加异常处理的复杂度;开发者通常使用多个 try-except 块来实现简单的顺序异常处理。

注意:您也可以在 try 或 except 下添加嵌套的 try-except 块,具体取决于您的需求。

在 Python 中抛出异常

除了捕获异常之外,您还可以使用 raise 关键字主动抛出异常。这对于执行业务规则或输入校验很有用,可以在问题进一步传导并导致更难调试的失败之前发出信号。

要抛出异常,需要使用 raise 关键字并跟随异常名称。

抛出 ValueError 示例

我们可以在 if/else 语句中直接使用 raise 抛出异常。

如下示例中,若值超过 1,000 则抛出 ValueError。我们把值改为 2,000,使 if 条件为 TRUE,并抛出带自定义消息的 ValueError。自定义错误消息有助于快速定位问题。

value = 2_000
if value > 1_000:   
    # raise the ValueError
    raise ValueError("Please add a value lower than 1,000")
else:
    print("Congratulations! You are the winner!!")
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)

----> 4     raise ValueError("Please add a value lower than 1,000")
      5 else:
      6     print("Congratulations! You are the winner!!")

ValueError: Please add a value lower than 1,000

抛出异常示例

只要条件满足,我们也可以抛出任意内置异常。在此示例中,我们抛出了一个通用 Exception 并携带错误消息。

if value > 1_000:   
    # raise the Exception
    raise Exception("Please add a value lower than 1,000")
else:
    print("Congratulations! You are the winner!!")
---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
----> 3     raise Exception("Please add a value lower than 1,000")
      4 else:
      5     print("Congratulations! You are the winner!!")

Exception: Please add a value lower than 1,000

处理已抛出的异常示例

我们也可以自定义异常,并用 try-except 来处理。

在此示例中,我们把抛出 ValueError 的逻辑放在 try 语句下。

它如何工作?不是抛出异常并终止程序,而是显示我们提供的错误消息。

value = 2_000
try:
    if value > 1_000:
          
        # raise the ValueError
        raise ValueError("Please add a value lower than 1,000")
    else:
        print("Congratulations! You are the winner!!")
              
# if false then raise the value error
except ValueError as e:
        print(e)

这种异常处理有助于我们为 Python 未涵盖、但特定于您应用需求的错误做好准备。

Please add a value lower than 1,000

使用 raise ... from 进行异常链

除了单独抛出异常外,还可以使用 raise ... from 进行异常链。这样会保留原始异常作为上下文,使当一个错误引发另一个错误时,回溯信息更清晰。

try:
    open("config.json")
except FileNotFoundError as e:
    raise RuntimeError("App failed to start: config file missing") from e
FileNotFoundError: [Errno 2] No such file or directory: 'config.json'

The above exception was the direct cause of the following exception:

RuntimeError: App failed to start: config file missing

其中 from e 会在新异常上设置 __cause__ 属性,使阅读回溯的人可以明确看到其触发原因。这在大型应用中尤为有用,因为异常会跨越多个代码层级。

Python 3.10 - 3.13 中异常处理的新变化

自本文最初发布以来,较新的 Python 版本(如 3.10 与 3.11)对异常处理引入了一些重要改进:

1. Python 3.10 的结构化模式匹配

Python 3.10 引入了结构化模式匹配,可在 match 语句中按错误类型匹配,从而更优雅地处理异常。对于需要处理多种异常类型、且希望可读性更强的场景尤其有用。例如:

try:
    result = 1 / 0
except Exception as e:
    match e:
        case ZeroDivisionError():
            print("You cannot divide by zero.")
        case NameError():
            print("Variable not defined.")
        case _:
            print("An unexpected error occurred.")

2. 更精细的回溯定位(Python 3.11)

Python 3.11 的回溯现在能显示更精确的错误位置,使调试异常更容易。

在 Python 3.11 之前:

value = (1 + 2) * (3 / 0)
ZeroDivisionError: division by zero

在 Python 3.11 中,回溯会精确标注引发异常的子表达式:

ZeroDivisionError: division by zero
       (1 + 2) * (3 / 0)
                      ^

3. 异常组与 except*(Python 3.11)

Python 3.11 引入异常组,可使用 except* 在单个代码块中处理多个异常。这在管理异步代码或可能同时出现多个异常的场景中很有用。例如:

try:
    raise ExceptionGroup("Multiple errors", [ValueError("Bad value"), TypeError("Bad type")])
except* ValueError as ve:
    print(f"Handling ValueError: {ve}")
except* TypeError as te:
    print(f"Handling TypeError: {te}")

4. 异常的 add_note 方法(Python 3.11)

异常对象现在具有 add_note() 方法,开发者可以为异常添加自定义说明,便于调试。例如:

try:
    raise ValueError("Invalid input")
except ValueError as e:
    e.add_note("This happened while processing user input.")
    e.add_note("Consider validating input before processing.")
    raise
ValueError: Invalid input
Notes:
- This happened while processing user input.
- Consider validating input before processing.

5. PEP 654:更好地处理嵌套异常

借助异常组,嵌套异常在复杂工作流(如多进程或异步任务)中更易调试与处理。

6. 改进的错误消息(Python 3.12)

Python 3.12 延续了让错误消息更具体且可操作的长期改进。例如,NameError 现在会建议作用域内拼写相近的变量,而常见类型上的属性错误也会给出更有针对性的提示:

# Python 3.12+
my_list = [1, 2, 3]
my_list.appendd(4)
AttributeError: 'list' object has no attribute 'appendd'. Did you mean: 'append'?

7. 彩色回溯(Python 3.13)

Python 3.13 在交互式解释器中默认引入彩色回溯。错误类型、行号和消息以不同颜色高亮,尤其在包含多个异常链的长回溯中,有助于更快定位问题。

结论

单元测试(unit testing)与异常处理是 Python 编程的核心组成部分,使您的代码具备生产可用性并尽可能避免错误。本文学习了 Python 中的异常与错误,以及如何处理它们;同时了解了更复杂的嵌套 try-except 结构,并可按需创建自定义异常块。

这些工具与机制很重要,但多数情况下,简单的 try 与 except 块就能完成主要工作:try 负责捕获代码引发的异常,except 负责处理这些异常。

如果您仍感困惑、尚不知从何入手,建议学习我们的 Introduction to Functions in Python 课程,以理解作用域、lambda 函数与错误处理。您也可以加入 Python Programmer 职业路径,系统提升技能,成为专业的 Python 开发者。

Abid Ali Awan

作者

Abid Ali Awan

作为一名持证的数据科学家,我热衷于利用前沿技术打造创新的机器学习应用。凭借在语音识别、数据分析与报告、MLOps、对话式人工智能以及自然语言处理方面的扎实背景,我不断打磨构建智能系统的能力,力求带来切实影响。除技术专长外,我也擅长沟通,能够将复杂概念提炼为清晰、简明的表述。因此,我成为数据科学领域备受关注的博主,与不断壮大的数据专业人士社区分享洞见与实践经验。目前,我专注于内容创作与编辑,借助大语言模型打造有力且吸引人的内容,帮助企业与个人更好地发挥数据价值。

FAQs

Python 中的错误与异常有何区别?

在 Python 中,错误通常是更严重的问题,会阻止程序继续运行,例如语法错误,表明代码结构不正确。相比之下,异常是一种会中断程序正常流程的情况,但可以通过 try-except 块在程序内部加以处理,从而使程序继续执行。

能否在单个 try-except 块中捕获多个异常?

可以。Python 允许您在单个 try-except 块中通过元组指定多个异常类型。例如: except (TypeError, ValueError):。这样可在同一代码块中处理 TypeError 或 ValueError。

如何在 Python 中创建自定义异常?

您可以通过定义继承自内置 Exception 类的新类来创建自定义异常。例如:

class MyCustomError(Exception):
    pass
在 try-except 结构中,else 块的作用是什么?

当 try 块未引发异常时,将执行 else 块。它适用于只应在 try 块成功时运行的代码,从而保持代码整洁,将错误处理与正常执行路径分离。

为什么 finally 块在异常处理中特别重要?

无论是否引发异常,finally 块总会执行。它通常用于清理操作,如关闭文件或释放资源,以确保这些动作在任何情况下都会执行。

如何在 Python 中抑制异常?

您可以使用 contextlib.suppress 上下文管理器来抑制异常。它允许您指定应被忽略的异常类型。例如:

from contextlib import suppress
with suppress(FileNotFoundError):
    open('non_existent_file.txt')
如果在 Python 程序中异常没有被捕获,会发生什么?

如果异常未被捕获,它会沿调用栈向上传播;若最终仍未处理,程序将终止,并在控制台打印回溯信息,显示异常发生的位置。

捕获异常后还能再次抛出吗?

可以。在 except 块中使用不带参数的 raise 语句即可重新抛出异常。这在您想记录错误或执行其他操作后,让异常继续向外传播时很有用。

是否有办法在 Python 中处理所有异常?

虽然可以使用裸 except: 捕获所有异常,但通常不建议这么做,因为它会捕获意料之外的异常,增加调试难度。更好的方式是捕获具体的异常类型,或使用 except Exception:,以避免捕获诸如 SystemExit 和 KeyboardInterrupt 等会导致程序退出的异常。

从发生时机看,语法错误与运行时异常有何不同?

语法错误在“编译时”被检测,即 Python 解释器在程序开始执行前就会捕获这些错误。运行时异常发生在程序执行期间,当解释器遇到无法执行的操作(例如除以零或访问不存在的变量)时会出现。

主题