数据处理时,情况并非总如预期。文件可能缺失,数据格式可能异常,网络资源可能不可用,或者计算上可能出现不可能的情况(例如除以零)。如果没有处理这些情况的机制,你的Python脚本将简单地崩溃,常常导致数据处理流程处于不完整或不一致的状态。异常处理提供了一种结构化的方法来预测并处理这些错误,使你的程序能够正常恢复或以可控方式终止。Python使用 try 和 except 块来管理异常。可能引发错误的代码放在 try 块内。如果该块内发生错误,Python会查找对应的 except 块来处理它。基础的 try...except 块基本结构是将可能出问题的代码置于 try 子句中,错误处理逻辑置于 except 子句中。try: # 可能引发异常的代码 value = int("this is not an integer") print("转换成功!") # 此行不会执行 except ValueError: # 发生 ValueError 时执行的代码 print("捕获到 ValueError:无法将字符串转换为整数。") print("处理异常后程序继续执行。")在此示例中,尝试使用 int() 将字符串 "this is not an integer" 转换为整数会引发 ValueError。由于导致错误的代码在 try 块内部,Python不会立即停止。相反,它会寻找与错误类型 (ValueError) 匹配的 except 块。它找到了一个,执行该 except 块中的代码,然后继续执行整个 try...except 结构之后的代码。处理特定异常虽然你可以使用裸露的 except: 子句来捕获任何异常,但这通常不被建议。捕获过于宽泛的异常可能会隐藏错误,或使难以弄清楚程序为何失败。更好的做法是仅捕获你预期并知道如何处理的特定异常。在数据处理中你可能会遇到的常见异常包括:FileNotFoundError:尝试打开不存在的文件。PermissionError:尝试在没有所需权限的情况下读/写文件。ValueError:操作接收到正确类型但值不合适的参数(例如,int('abc'))。TypeError:对不合适类型的对象执行操作(例如,len(123))。KeyError:尝试访问字典中不存在的键。IndexError:尝试使用越界索引访问列表元素。ZeroDivisionError:尝试除以零或取模操作。你可以在 except 关键词后指定要捕获的异常类型:filename = "non_existent_file.csv" try: with open(filename, 'r') as f: content = f.read() print("文件读取成功。") except FileNotFoundError: print(f"错误:文件 '{filename}' 未找到。") except PermissionError: print(f"错误:没有权限读取 '{filename}'。") # 不同错误类型的示例 data = {'a': 1, 'b': 0} key = 'c' try: value = data[key] result = 10 / value # 如果值为0,可能引发 KeyError 或 ZeroDivisionError print(f"'{key}' 的结果:{result}") except KeyError: print(f"错误:字典中未找到 '{key}'。") except ZeroDivisionError: print(f"错误:不能除以零(键关联的值为0)。")处理多个异常如果你需要对几种不同的异常类型执行相同的处理逻辑,可以将它们分组放在一个 except 子句的元组中:numerator = 100 denominator_str = "0" # 也可以是 "abc" 或一个有效数字 try: denominator = int(denominator_str) result = numerator / denominator print(f"结果:{result}") except (ValueError, ZeroDivisionError) as e: # 捕获其中任一错误 # 'as e' 部分将异常对象赋值给变量 'e' print(f"计算过程中发生错误:{e}") print(f"错误类型:{type(e).__name__}")使用 as e 有助于记录具体的错误消息或检查异常对象本身。else 子句有时,你可能有一些代码,它们应该 只在 try 块成功完成(即没有引发异常)时运行。这就是可选的 else 子句的作用。try: # 尝试打开并读取数据 file = open("data.txt", "r") data = file.read() except FileNotFoundError: print("错误:data.txt 未找到。") # 处理错误,或许将数据设置为默认值 data = None else: # 只有当 try 块成功时才运行此块 print("文件读取成功。") # 对成功读取的数据进行操作 processed_data = data.upper() print(f"处理后的数据:{processed_data[:50]}...") finally: # 始终运行的清理代码(参见下一节) if 'file' in locals() and file: # 检查文件是否已打开 file.close() print("文件已关闭。")else 块中的代码不受前面 except 子句处理的异常影响。如果你将处理代码直接放在 file.read() 之后的 try 块内部,那么如果使用了不太具体的 except 子句,处理过程中发生的错误可能被 FileNotFoundError 处理器错误地捕获。finally 子句还有一个可选子句:finally。finally 块内的代码 总是 执行,无论 try 块中是否发生异常,或者是否被 except 块处理。即使 try 或 except 块使用了 return、break 或 continue,它也会运行。这使其非常适合用于清理操作,例如关闭文件或释放资源。file = None # 在 try 块外部初始化变量 try: file = open("important_resource.lock", "w") # 执行可能失败的操作... print("尝试关键操作...") # 模拟一个错误 # raise ValueError("操作过程中发生错误!") print("关键操作成功。") except Exception as e: print(f"捕获到错误:{e}") # 处理或记录错误 finally: print("执行 finally 块。") if file: # 确保文件已成功打开再尝试关闭 file.close() print("资源已关闭。") else: print("资源未打开。")请注意,finally 块确保无论操作成功或失败,文件都会被关闭(如果它已打开)。虽然 finally 功能强大,但请记住,上下文管理器(使用前面讨论的 with 语句)通常是管理资源(尤其是文件)更具 Python 风格且更易读的方式,因为即使发生错误,它们也会隐式处理关闭操作。抛出异常你不仅限于处理 Python 内置函数或库引发的异常。你也可以使用 raise 语句自己引发异常。这对于标记你自己的代码逻辑检测到的错误情况很有用。你可以引发内置的异常类型,也可以定义自己的自定义异常类(尽管这超出了我们当前的讨论范围)。def calculate_normalized_value(value, maximum): if not isinstance(value, (int, float)) or not isinstance(maximum, (int, float)): raise TypeError("值和最大值都必须是数字。") if maximum <= 0: # 抛出一个具体且有信息的错误 raise ValueError("最大值必须为正数才能进行归一化。") if value < 0 or value > maximum: raise ValueError("值必须在 0 和最大值之间(含)。") return value / maximum try: norm_val = calculate_normalized_value(15, 10) # 无效值 print(f"归一化值:{norm_val}") except (TypeError, ValueError) as e: print(f"归一化失败:{e}") try: norm_val = calculate_normalized_value(5, -2) # 无效最大值 print(f"归一化值:{norm_val}") except (TypeError, ValueError) as e: print(f"归一化失败:{e}") try: norm_val = calculate_normalized_value(7, 10) # 有效输入 print(f"归一化值:{norm_val}") except (TypeError, ValueError) as e: print(f"归一化失败:{e}")抛出特定的异常使你的函数能够清晰地向调用它们的代码传达错误信息。数据管道中的异常处理在机器学习和数据科学的工作流程中,数据通常会经过多个处理步骤。每个阶段的异常处理都非常重要。以一个数据管道为例,它读取数据、清理数据、转换特征,然后将其输入模型。如果清理步骤中出现错误(例如,数字列中出现意外的字符串),只要你能处理它(例如,通过记录有问题的数据行并跳过,或填充一个值),就不应因此停止整个过程。def safe_process_row(row_data, row_number): """处理单行数据,同时处理潜在错误。""" try: # 示例:转换特定列,执行计算 col1_val = float(row_data.get('feature1', '0')) # 如果缺失,默认为 '0' col2_val = float(row_data.get('feature2', '0')) if col2_val == 0: raise ValueError("Feature2 不能为零进行比率计算。") processed_value = col1_val / col2_val # ... 更多处理 ... return {'processed': processed_value, 'status': 'success'} except (ValueError, TypeError) as e: print(f"警告:由于错误,跳过第 {row_number} 行:{e}") return {'data': row_data, 'status': 'error', 'reason': str(e)} except Exception as e: # 捕获意外错误 print(f"错误:处理第 {row_number} 行时发生意外问题:{e}") return {'data': row_data, 'status': 'error', 'reason': f'意外:{e}'} # 设想遍历原始数据行(例如,字典列表) raw_data = [ {'feature1': '10', 'feature2': '2'}, {'feature1': 'abc', 'feature2': '5'}, # 将导致 float() 上的 TypeError/ValueError {'feature1': '8'}, # 缺少 feature2 -> 使用默认值 '0' -> ValueError {'feature1': '20', 'feature2': '4'}, ] processed_results = [] error_rows = [] for i, row in enumerate(raw_data): result = safe_process_row(row, i + 1) if result['status'] == 'success': processed_results.append(result['processed']) else: error_rows.append(result) print(f"\n成功处理 {len(processed_results)} 行。") print(f"在 {len(error_rows)} 行中遇到错误。") # 可选地检查或保存 error_rows 以供后续分析通过引入 try...except 块,你可以构建更具适应性的数据处理应用程序,它们能够处理不完善的数据和意外情况而不会崩溃,并在此过程中提供有价值的反馈。这种做法对于创建可靠的机器学习系统非常重要。