Python适用于多种条件的设计模式

15
什么是编写包含多个条件的验证函数的推荐结构?请看这两个例子。第一个看起来很丑,第二个并不常见,可能是因为通常使用"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

23
不应使用assert来验证数据!它用于验证程序的逻辑:您使用它来测试某些条件,如果您的程序逻辑正确,这些条件将永远不会发生。如果程序引发AssertionError,那么就意味着您的代码存在错误并需要调试。 - PM 2Ring
11
此句话的意思是:如果代码被优化,会删除断言语句。 - Peter Wood
@PeterWood 确实!事实上,在看到您的评论之前,我已经向我的答案中添加了该信息。;) - PM 2Ring
如果你正在编写将由他人维护的代码,我会使用第一种方式,因为它非常易读。在代码中运用诗意可以满足智力上的需求并展现出敏锐的思维,但可能会带来后患。 - copper.hat
3
“众所周知,调试程序比起一开始编写程序时要困难两倍。因此,如果你在编写时已经尽可能聪明,那么你将如何调试呢?” ——布莱恩·克尼根,《程序设计风格之元素》第二版,第二章。 - PM 2Ring
@PM2Ring:我知道这个问题。曾经被它咬过。但是一个月前写的代码现在还是有点难以理解。 - copper.hat
3个回答

41

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

def validate(val):
    conditions = (cond1, cond2, cond3)
    return not any(cond(val) for cond in conditions)
< p > anyall 函数是短路运算,所以它们一旦有了明确的结果就会停止测试,即 any 遇到 True-ish 值就停止,all 遇到 False-ish 值就停止,因此这种测试形式非常高效。

我还应该提到,将生成器表达式传递给 all/any 比传递列表推导式更加高效。因为 all/any 一旦得到有效结果就停止测试,如果你从一个生成器中提供数据给它们,那么生成器也会停止,因此在上面的代码中,如果 cond(val) 的值为 True-ish,则不会再进行任何测试。但是如果你传递一个列表推导式给 all/any ,例如 any([cond(val) for cond in conditions]) ,则整个列表必须被构建,然后 all/any 才能开始测试。


您没有展示您的cond函数的内部结构,但是在您的问题中提到了assert,因此我认为以下的评论在这里是有必要的。
如我在评论中所述,assert不应用于数据验证,它用于验证程序逻辑。(此外,可以通过-O命令行选项禁用断言处理)。对于具有无效值的数据,应使用正确的异常ValueError,对于类型错误的对象,使用TypeError。但请记住,异常是设计用于处理非常规情况的。
如果您预计会有大量格式不正确的数据,则通常使用基于if的逻辑比使用异常更有效。Python的异常处理速度很快,如果实际上未引发异常,事实上它比等效的基于if的代码更快。但是,如果异常被引发的次数超过5-10%,那么try...except的代码将比等效的基于if的代码明显慢。
当然,有时即使情况并不是非常特殊,使用异常也是唯一明智的选择。一个经典的例子是当您将一组数字字符串转换为实际的数字对象时,使表示整数的字符串转换为整数对象,其他数字字符串转换为浮点数,其他字符串保留为字符串。在Python中执行此操作的标准方法涉及使用异常。例如:
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'>]

在这里使用"三思而后行"的逻辑是可能的,但它会使代码变得更加复杂,因为您需要处理可能存在的减号和科学计数法。


1
any 可能会短路,但是在构建元组时条件已经被评估,因此它不会像通常的逻辑运算那样短路评估。 - Kroltan
4
cond1cond2cond3 是函数。就调用哪些函数而言,这段代码与原始代码是等效的。 - Theodore Norvell
2
@wchargin 看得更仔细一些。首先,anyall都会进行短路计算。其次,元组包含的是函数,而不是调用这些函数后的结果。此外,传递给any的参数是一个生成器。只有在通过迭代到达它们时,才会调用这些函数,而生成器是由anyall函数内部推进的(它们一旦返回就停止推进)。 - jpmc26
1
另一方面,有一个名为timeit的测试,比较了在更新用作缓存的字典时使用inget的速度。 get通过捕获KeyError异常来工作,但它是在“幕后”进行的,因此可以比纯Python代码更有效地工作。 - PM 2Ring
1
@jpmc26:你说得完全正确。是我的错误。(我错过了cond_i是函数而不是布尔值;其余部分很明显。)谢谢。 - wchargin
显示剩余4条评论

5
def valid(value):
    return (is_this(value)
            and that(value)
            and other(value))

and 操作符在 Python 中表现出“短路”行为。


2
当所有条件都为True时,它将返回True。但是如果任何一个条件为True,则应该返回False - Peter Wood
2
@PeterWood 是的,这是意图。条件被颠倒了(看名字)。要使用OP的条件和名称:return not (cond1(val) or cond2(val) or cond3(val)) 我认为我的变体更易读。 - jfs

2
第一种方法更好。可以使用any()稍微美化一下:
def validate_conditions(value):
    return not any((condition(value) for condition in conditions))

1
当所有条件都为True时,它将返回True。但是如果任何一个条件为True,则应该返回False - Peter Wood
3
еҪјеҫ—иҜҙзҡ„иҜқгҖӮеҗҢж—¶пјҢе°Ҷз”ҹжҲҗеҷЁиЎЁиҫҫејҸдј йҖ’з»ҷall/anyжҜ”е°ҶеҲ—иЎЁжҺЁеҜјејҸдј йҖ’иҰҒжӣҙеҠ й«ҳж•ҲгҖӮ all/anyеңЁиҺ·еҫ—жңүж•Ҳз»“жһңеҗҺз«ӢеҚіеҒңжӯўжөӢиҜ•пјҢеӣ жӯӨеҰӮжһңжӮЁд»Һз”ҹжҲҗеҷЁдёӯжҸҗдҫӣж•°жҚ®пјҢеҲҷз”ҹжҲҗеҷЁд№ҹдјҡеҒңжӯўгҖӮдҪҶжҳҜпјҢеҰӮжһңжӮЁеҗ‘е®ғ们传йҖ’еҲ—иЎЁжҺЁеҜјејҸпјҢеҲҷеҝ…йЎ»е…Ҳжһ„е»әж•ҙдёӘеҲ—иЎЁпјҢ然еҗҺall/anyжүҚиғҪејҖе§ӢжөӢиҜ•гҖӮ - PM 2Ring
感谢您澄清。 - Evya
此外,在Python中,生成器表达式是少数几个鼓励不那么隐式的东西之一。((...))可以被(...)替换,换句话说,如果生成器表达式是唯一的参数,则不需要括号。 - Graipher

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接