【问题标题】:Python design pattern for many conditions适用于多种条件的 Python 设计模式
【发布时间】:2017-12-09 12:00:44
【问题描述】:

编写具有多种条件的验证函数的推荐结构是什么?请参阅这两个示例。第一个看起来很丑,第二个不太常见,可能是因为assert 通常用于排除意外行为。有更好的选择吗?

def validate(val):
  if cond1(val):
    return False
  if cond2(val):
    return False
  if cond3(val)
    return False
  return True

或者

def validate(val):
  try:
    assert cond1(val)
    assert cond2(val)
    assert cond3(val)
    return True
  except AssertionError:
    return False

【问题讨论】:

  • 您应该使用assert 来验证数据!它用于验证程序的逻辑:如果您的程序逻辑正确,您可以使用它来测试应该永远发生的条件。如果程序引发AssertionError,则意味着您的代码错误并且需要调试。
  • @PM2Ring 另外,如果代码优化assert statements are removed.
  • @PeterWood 确实!在看到您的评论之前,我实际上已将该信息添加到我的答案中。 ;)
  • 如果您编写的代码将由其他人维护,我会使用第一个,因为它非常易读。代码中的诗歌在智力上令人满意,并且表现出敏锐的头脑,但可能会再次困扰。
  • @copper.hat “每个人都知道,调试的难度是编写程序的两倍。所以,如果你在编写程序时尽可能地聪明,你会怎么做调试它?” — Brian Kernighan,编程风格的要素,第 2 版,第 2 章。

标签: python design-patterns


【解决方案1】:

编写该函数的一种简洁方式是使用any 和生成器表达式:

def validate(val):
    conditions = (cond1, cond2, cond3)
    return not any(cond(val) for cond in conditions)

anyall 函数短路,因此一旦得到明确结果,它们就会停止测试,即,any 在达到 True-ish 值时立即停止,@987654329 @ 在达到 False-ish 值时立即停止,因此这种形式的测试非常有效。

我还应该提到,将这样的生成器表达式传递给all / any 比列表推导式更有效。因为all / any 在得到有效结果后立即停止测试,如果您从生成器提供它们,那么生成器也会停止,因此在上面的代码中,如果cond(val) 评估为 True-ish 值 no将测试进一步的条件。但是如果你通过all / any 一个列表解析,例如any([cond(val) for cond in conditions]) 整个列表必须在all / any 甚至可以开始测试之前构建。


您没有向我们展示您的cond函数的内部结构,但您在问题中确实提到了assert,所以我觉得下面的说明是按顺序排列的。

正如我在 cmets 中提到的,assert 不应该用于验证数据,它用于验证程序逻辑。 (此外,可以通过 -O 命令行选项禁用断言处理)。用于具有无效值的数据的正确异常是ValueError,对于错误类型的对象,使用TypeError。但请记住,异常旨在处理异常情况。

如果您预计会有大量格式错误的数据,那么使用基于 if 的逻辑通常比使用异常更有效。如果实际上没有引发异常,Python 异常处理非常快,实际上它比基于 if 的等效代码要快。但是,如果出现异常的概率超过 5-10%,则基于 try...except 的代码将明显慢于基于 if 的等效代码。

当然,有时使用异常是唯一明智的选择,即使情况并非那么异常。一个典型的例子是,当您将一组数字字符串转换为实际的数字对象时,表示整数的字符串会转换为整数对象,其他数字字符串会转换为浮点数,而其他字符串则保留为字符串。在 Python 中执行此操作的标准方法涉及使用异常。 For example:

def convert(s):
    ''' Convert s to int or float, if possible '''
    try:
        return int(s)
    except ValueError:
        try:
            return float(s)
        except ValueError:
            return s

data = ['42', 'spam', '2.99792458E8']
out = [convert(u) for u in data]
print(out)
print([type(u) for u in out])

输出

[42, 'spam', 299792458.0]
[<class 'int'>, <class 'str'>, <class 'float'>]

在此处使用"Look Before You Leap" 逻辑是可能,但它会使代码更加复杂,因为您需要处理可能的减号和科学记数法。

【讨论】:

  • 当然,这些并不完全等效,因为你没有短路。如果cond1not xscond2xs[0] == 'bad',那么将[] 传递给xs 将导致错误。
  • any 可能会短路,但是在构造元组时已经评估了条件,因此它不会像通常的逻辑那样真正短路评估
  • cond1cond2cond3 是函数。这段代码在调用哪些函数方面与原来的代码是等价的。
  • @wchargin 仔细看。首先,anyall do 都短路了。其次,元组包含函数,而不是调用这些函数的结果。此外,传递给any 的参数是一个生成器。这些函数在通过生成器迭代到达它们之前不会被调用,并且生成器在内部由anyall 函数推进(它们一返回就停止推进)。
  • @jpmc26 (cont) OTOH,this timeit test 比较了 inget 的速度,用于更新用作缓存的字典。 get 通过捕获 KeyError 异常来工作,但它是在“幕后”进行的,因此它可以比纯 Python 代码更有效地工作。
【解决方案2】:
def valid(value):
    return (is_this(value)
            and that(value)
            and other(value))

and 运算符在 Python 中表现出“短路”行为。

【讨论】:

  • all 条件评估为True 时,这将返回True。但如果any 条件评估True,它应该返回False
  • @PeterWood 是的,这就是目的。条件颠倒了(看名字)。要使用 OP 的条件和名称:return not (cond1(val) or cond2(val) or cond3(val)) 我发现我的变体更具可读性。
【解决方案3】:

第一种方法要好得多。可以使用any() 美化一点:

def validate_conditions(value):
    return not any((condition(value) for condition in conditions))

【讨论】:

  • all 条件评估为True 时,这将返回True。但如果any 条件评估True,它应该返回False
  • 彼得所说的。此外,将生成器表达式传递给all / any 比列表推导式更有效all / any 一旦得到有效结果就停止测试,所以如果你从生成器给它们喂食,那么生成器也会停止。但是如果你给他们一个列表组合,整个列表必须在all/any甚至可以开始测试之前构建。
  • 感谢您的澄清
  • 另外,生成器表达式是少数在 Python 中鼓励不那么隐式的东西之一。 ((...)) 可以替换为 (...),或者换句话说,如果生成器表达式是唯一的参数,则不需要大括号。
猜你喜欢
  • 1970-01-01
  • 2021-09-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-02-27
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多