使用try/catch还是验证来提高速度?

36 投票
2 回答
8290 浏览
提问于 2025-04-16 15:19

我在用Python编程,每次需要验证函数输入的时候,我通常是先假设输入是正确的,然后再捕捉错误。

在我的例子中,我有一个通用的 Vector() 类,用于处理几种不同的事情,其中之一就是加法。这个类既可以作为 Color() 类使用,也可以作为 Vector() 使用,所以当我把一个数加到 Color() 时,它应该把这个常数加到每个单独的组成部分上。而 Vector()Vector() 的加法则需要逐个组成部分相加。

这段代码是用在光线追踪器里的,所以任何速度上的提升都是很重要的。

下面是我简化版的 Vector() 类:

class Vector:
  def __init__(self, x, y, z):
    self.x = x
    self.y = y
    self.z = z

  def __add__(self, other):
    try:
      return Vector(self.x + other.x, self.y + other.y, self.z + other.z)
    except AttributeError:
      return Vector(self.x + other, self.y + other, self.z + other)

我现在使用的是 try...except 方法。有没有人知道更快的方法呢?


编辑:感谢大家的回答,我尝试并测试了以下解决方案,它在添加 Vector() 对象之前,专门检查类名:

class Vector:
  def __init__(self, x, y, z):
    self.x = x
    self.y = y
    self.z = z

  def __add__(self, other):
    if type(self) == type(other):
      return Vector(self.x + other.x, self.y + other.y, self.z + other.z)
    else:
      return Vector(self.x + other, self.y + other, self.z + other)

我用 timeit 进行了这两段代码的速度测试,结果非常明显:

 1.0528049469 usec/pass for Try...Except
 0.732456922531 usec/pass for If...Else
 Ratio (first / second): 1.43736090753

我还没有测试过 Vector() 类完全不进行输入验证的情况(也就是把检查移出类,放到实际代码中),但我想这会比 if...else 方法更快。


后续更新:回头看这段代码,这并不是一个最优的解决方案。

面向对象编程(OOP)让这个过程更快:

class Vector:
  def __init__(self, x, y, z):
    self.x = x
    self.y = y
    self.z = z

  def __add__(self, other):
    return Vector(self.x + other.x, self.y + other.y, self.z + other.z)

class Color(Vector):
  def __add__(self, other):
    if type(self) == type(other):
      return Color(self.x + other.x, self.y + other.y, self.z + other.z)
    else:
      return Color(self.x + other, self.y + other, self.z + other)

2 个回答

6

在Python中,处理异常通常会更快,因为需要查找的东西比较少。不过,有个朋友曾经说过(这应该适用于任何编程语言),可以想象每次捕获到异常时,都会有一点小延迟。所以,如果你觉得延迟会造成问题,就尽量避免使用异常。

在你给的例子中,我会选择使用异常。

83

我赞同Matt Joiner的回答,但想补充一些观察,以便更清楚地说明,在选择预先检查条件(称为LBYL或“先看后跳”)和处理异常(称为EAFP或“更容易请求原谅而不是请求许可”)时,有4个时间点是非常重要的。

这四个时间点是:

  • 使用LBYL时检查成功的时间
  • 使用LBYL时检查失败的时间
  • 使用EAFP时没有抛出异常的时间
  • 使用EAFP时抛出异常的时间

还有一些额外的因素:

  • 检查成功/失败或异常抛出/未抛出的典型比例
  • 是否存在竞争条件,导致无法使用LBYL

最后一点需要优先考虑:如果存在潜在的竞争条件,那么你别无选择,必须使用异常处理。一个经典的例子是:

if <dir does not exist>:
    <create dir> # May still fail if another process creates the target dir

因为在这种情况下,LBYL并不能排除异常的发生,所以它没有实际的好处,也没有什么判断可做:EAFP是唯一能正确处理竞争条件的方法。

但是如果没有竞争条件,两种方法都有可能有效。它们各有优缺点:

  • 如果没有抛出异常,那么EAFP几乎是免费的
  • 然而,如果发生异常,它的成本相对较高,因为需要处理很多内容,比如回溯栈、创建异常以及与异常处理条款进行比较
  • 相比之下,LBYL可能会产生较高的固定成本:无论成功与否,额外的检查总是会执行

这就引出了以下决策标准:

  • 这段代码是否被认为对应用程序的速度至关重要?如果不是,那就不用担心哪种方法更快,而是关注哪种方法更容易阅读。
  • 预检查的成本是否高于抛出和捕获异常的成本?如果是,那么EAFP总是更快,应该使用。
  • 如果答案是否定的,情况就更有趣了。在这种情况下,哪种方法更快将取决于成功情况还是错误情况更常见,以及预检查和异常处理的相对速度。要明确回答这个问题,需要进行实际的时间测量。

作为一个粗略的经验法则:

  • 如果存在潜在的竞争条件,使用EAFP
  • 如果速度不是关键,随便用你认为更容易阅读的那种
  • 如果预检查的成本很高,使用EAFP
  • 如果你预计操作大多数时候会成功*,使用EAFP
  • 如果你预计操作失败的次数超过一半,使用LBYL
  • 如果不确定,就测量一下

*人们对“多数时候”的定义可能会有所不同。对我来说,如果我预计操作成功的次数超过一半,我通常会使用EAFP,直到我有理由怀疑这段代码是一个实际的性能瓶颈。

撰写回答