PEP 8 – Python 代码风格指南中文版(七)

  • 编程建议(2)

 

  • 定义异常时,应该从Exception类继承,而不是从BaseException类继承。直接从BaseException继承的异常通常是那些几乎不应该被捕获的异常。

设计异常层次结构时,应该基于捕获异常的代码可能需要进行的区分,而不是基于异常被抛出的位置。目标是通过编程方式回答“出了什么问题?”这个问题,而不仅仅是声明“发生了一个问题”。(关于这一点,你可以查看PEP 3151来了解内置异常层次结构是如何学习这一教训的)。

类命名约定在这里也适用,但是如果你定义的异常是一个错误,你应该在异常类名后面添加“Error”后缀。那些用于非局部流程控制或其他信号形式的非错误异常则不需要特殊后缀。

解释:

在Python中,BaseException是所有异常的基类,包括ExceptionSystemExitKeyboardInterrupt等。通常,我们定义的自定义异常应该从Exception类继承,因为Exception及其子类是用于程序运行时出现的、需要被捕获和处理的情况。而直接从BaseException继承的异常应该非常谨慎地使用,因为这类异常通常表示了非常底层或严重的问题,比如程序被强制退出或用户中断了程序。

在设计异常类时,应该考虑到你的异常层次结构如何帮助调用者(即捕获异常的代码)理解发生了什么问题,并据此做出适当的响应。这意味着你应该根据错误的性质、严重性和对程序的影响来组织你的异常类。例如,你可能会有一个表示数据库错误的基类DatabaseError,然后从这个基类派生出更具体的异常类,如ConnectionErrorQueryError等。这样,当捕获到这些异常时,调用者可以根据异常的具体类型来做出不同的处理。

PEP 3151是一个关于Python标准库中异常重构的提案,它提供了关于如何设计更加清晰和有用的异常层次结构的例子。通过阅读这个PEP,你可以了解到Python核心开发者是如何思考和改进内置异常的,从而为你的自定义异常设计提供灵感。

在命名异常类时,你应该遵循Python的命名约定,即使用驼峰命名法(CamelCase)或下划线命名法(snake_case),具体取决于你的项目或团队的约定。对于表示错误的异常,添加“Error”后缀可以帮助调用者更容易地识别出这是一个错误情况。然而,如果你定义的异常不是用来表示错误的(比如,用于控制程序流程的信号),那么你就不需要添加这样的后缀。

  • 适当使用异常链。 使用 raise X from Y 应该表明明确的替换,同时不会丢失原始的跟踪信息。

当故意替换内部异常(使用 raise X from None)时,请确保将相关细节转移到新的异常中(例如,在将 KeyError 转换为 AttributeError 时保留属性名,或在新的异常消息中嵌入原始异常的文本)。

解释:

Python中,raise X from Y语法允许你抛出一个新的异常X,并指明它是从另一个异常Y中“衍生”出来的。这样做的好处是,当你捕获并处理X时,你仍然可以通过X.__cause__属性访问到原始的异常Y,以及它的跟踪信息。这在你需要记录或调试复杂的错误情况时非常有用。

有时候,你可能需要捕获一个异常,并根据情况抛出一个新的异常,但你不想保留原始异常的跟踪信息。这时,你可以使用raise X from None来明确表示你正在替换异常,并且不想包含任何原始跟踪信息。然而,这并不意味着你应该完全忽略原始异常的信息。相反,你应该确保新异常包含了足够的信息,以便调用者能够理解发生了什么,以及原始异常是如何影响新异常的。这通常意味着你需要在新异常的消息中包含原始异常的文本,或者在转换异常类型时保留一些关键信息(如属性名)。

  • 在捕获异常时,尽可能提到具体的异常,而不是使用裸露的 except: 子句:

try:
    import platform_specific_module
except ImportError:
    platform_specific_module = None

解释:

在Python中,try...except语句用于捕获并处理代码块中可能发生的异常。如果try块中的代码引发了异常,并且这个异常与except子句后面指定的异常类型相匹配,那么就会执行except块中的代码。

  • 使用具体的异常类型:在上面的例子中,except ImportError:指定了只有当try块中的代码引发ImportError时,才会执行except块中的代码。这是一个好的做法,因为它使得代码的意图更加清晰,并且避免了捕获和处理那些你不打算处理的异常。

  • 避免使用裸露的except:子句:裸露的except:子句(即没有指定任何异常类型的except子句)会捕获所有类型的异常。虽然这看起来很强大,但实际上它会隐藏潜在的问题,使得调试变得更加困难。此外,它还可能导致程序在不应该继续执行的情况下继续执行,从而引入新的错误。

裸露的 except: 子句会捕获 SystemExit 和 KeyboardInterrupt 异常,这使得使用 Control-C 中断程序变得困难,并可能掩盖其他问题。如果你想要捕获所有表示程序错误的异常,应该使用 except Exception:(裸露的 except 相当于 except BaseException:)。

一个大致的指导原则是,将裸露的 ‘except’ 子句的使用限制在以下两种情况:

  1. 如果异常处理程序将打印或记录跟踪信息;至少用户会意识到发生了错误。
  2. 如果代码需要进行一些清理工作,但随后允许异常向上传播(使用 raise)。对于这种情况,try...finally 可能是更好的处理方式。

解释:

  1. 打印或记录异常跟踪信息

在某些情况下,你可能只想让用户知道程序遇到了一个错误,但并不打算立即处理这个错误(比如,你可能不知道如何处理它,或者它是一个不可恢复的错误)。此时,你可以在except块中打印或记录异常的跟踪信息,以便后续分析和调试。但是,这并不意味着你应该总是这样做,因为这样做可能会隐藏程序中的其他问题,或者导致用户看到不必要的错误信息。

  1. 进行清理工作并让异常继续传播

有时候,即使发生了异常,你也可能需要执行一些清理工作(比如关闭文件、释放资源等)。但是,你并不打算在这个级别上处理异常,而是希望让调用者知道发生了错误。此时,你可以在except块中进行清理工作,并使用raise语句重新抛出异常。然而,对于这种情况,使用try...finally结构通常是一个更好的选择,因为它可以确保无论是否发生异常,清理代码都会被执行。

裸露的except子句应该谨慎使用,因为它们会捕获所有类型的异常,这可能会隐藏潜在的问题。在大多数情况下,你应该指定具体的异常类型,以便更精确地控制异常处理逻辑。如果你确实需要使用裸露的except子句,请确保你清楚自己的目的,并考虑使用try...finally结构来进行必要的清理工作。

  • 在捕获操作系统错误时,应优先使用Python 3.3中引入的显式异常层次结构,而不是通过自省errno值。

解释:

在Python中,与操作系统交互时(如进行文件操作、网络请求等),可能会遇到各种由操作系统本身返回的错误。在早期的Python版本中,为了判断和处理这些错误,开发者可能需要直接检查errno的值(errno是一个全局变量,用于表示最近一次系统调用的错误代码)。然而,这种方法有几个缺点:首先,它不够直观,因为errno的值只是一些整数,没有直接说明错误的性质;其次,随着Python和操作系统的发展,errno的值可能会发生变化,这可能会导致代码与新的Python版本或操作系统版本不兼容。

为了解决这个问题,Python 3.3引入了一个更明确、更易于理解和使用的异常层次结构。这个层次结构包含了多个表示不同操作系统错误的异常类,这些类都是从OSError(或其子类FileNotFoundErrorPermissionError等)派生出来的。当发生操作系统错误时,Python会抛出相应的异常对象,而不是仅仅设置一个errno值。因此,开发者可以通过捕获这些具体的异常来更精确地处理错误,而不是通过检查errno的值来猜测发生了什么错误。

  • 此外,对于所有的 try/except 子句,应将 try 子句限制在绝对必要的最小代码量上。这样做同样是为了避免掩盖错误(bugs):

# 正确的:
try:
    value = collection[key]
except KeyError:
    return key_not_found(key)
else:
    return handle_value(value)


# 错误的:
try:
    # 范围太广了!
    return handle_value(collection[key])
except KeyError:
    # 这也会捕获由handle_value()函数抛出的KeyError
    return key_not_found(key)

解释:

当你使用try/except结构来捕获和处理异常时,你应该尽量将可能引发异常的代码放在try子句中,但同时也要避免将过多的无关代码也包含在内。原因主要有两点:

  1. 减少错误掩盖的风险:如果try子句中的代码过多,那么当异常发生时,你可能很难准确判断是哪一部分代码导致了问题。这可能会让你忽略掉一些潜在的错误(bug),因为异常处理器可能已经捕获并处理了这些错误,但你没有意识到。

  2. 提高代码的可读性和可维护性:将try子句中的代码量限制在最小范围内,可以使你的代码更加清晰和简洁。其他开发者(或未来的你)在阅读和修改代码时,可以更容易地理解你的意图,并找到可能的问题所在。

因此,在编写try/except子句时,你应该仔细考虑哪些代码可能会引发异常,并将这些代码放在try子句中。同时,确保try子句中的代码量尽可能少,以便在异常发生时能够迅速定位问题。

  • 当资源仅用于代码的特定部分时,应使用with语句来确保资源在使用后能够迅速且可靠地被清理。try/finally语句也是可接受的。

解释

在Python中,经常需要管理一些资源,比如文件句柄、网络连接、数据库连接等。这些资源在使用完毕后,通常需要进行一些清理工作,比如关闭文件、断开连接等,以确保资源得到正确释放,避免资源泄露。

with语句是Python提供的一种上下文管理器(context manager)机制,它可以帮助我们自动地管理资源。当你将一个资源放在一个with语句的上下文中时,Python会在进入该上下文时自动调用资源的__enter__方法(如果定义了的话),并在退出该上下文时自动调用资源的__exit__方法(无论是否发生异常)。这样,我们就可以在__exit__方法中编写资源清理的代码,从而确保资源得到及时且可靠的清理。

然而,并不是所有的资源都实现了上下文管理器接口(即定义了__enter____exit__方法)。在这种情况下,我们也可以使用try/finally语句来管理资源。在try子句中,我们执行需要资源的操作;在finally子句中,我们编写资源清理的代码。无论try子句中的代码是否发生异常,finally子句中的代码都会被执行,从而保证资源得到清理。

因此,PEP-8建议,在可能的情况下,使用with语句来管理资源,因为它更加简洁、易读,并且能够自动处理异常情况下的资源清理。但是,如果资源没有实现上下文管理器接口,那么使用try/finally语句也是一个不错的选择。

  • 当上下文管理器除了获取和释放资源外还执行其他操作时,应通过单独的函数或方法来调用它们:

# 正确的:
with conn.begin_transaction():
    do_stuff_in_transaction(conn)


# 错误的:
with conn:
    do_stuff_in_transaction(conn)

后一个示例没有提供任何信息来表明__enter__和__exit__方法除了在一个事务后关闭连接之外还执行了其他操作。在这种情况下,明确指出是很重要的。

解释

上下文管理器(context managers)在Python中通常用于管理资源的获取和释放。然而,有些上下文管理器可能不仅限于资源的获取和释放,它们还可能在进入和退出上下文时执行其他操作,比如启动和提交数据库事务。在这种情况下,PEP-8建议应该通过单独的函数或方法来调用这些上下文管理器,以便清晰地表明它们所执行的操作。

在上面的例子中,conn.begin_transaction()是一个假设的函数,它返回一个实现了上下文管理器接口的对象,该对象在__enter__方法中启动事务,在__exit__方法中提交或回滚事务。而直接使用conn作为上下文管理器则可能只会在__exit__方法中关闭连接,没有明确表示它还执行了事务操作。因此,使用conn.begin_transaction()作为上下文管理器是更明确、更易于理解的做法。

  相关文章:

PEP 8 – Python 代码风格指南中文版(一)

PEP 8 – Python 代码风格指南中文版(二)

PEP 8 – Python 代码风格指南中文版(三)

PEP 8 – Python 代码风格指南中文版(四)

PEP 8 – Python 代码风格指南中文版(五)

PEP 8 – Python 代码风格指南中文版(六)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值