我该如何调用一系列函数直到返回值满足某个条件?

10 投票
3 回答
5572 浏览
提问于 2025-04-18 05:10

有时候我发现自己写的代码像这样:

def analyse(somedata):
    result = bestapproach(somedata)
    if result:
        return result
    else:
        result = notasgood(somedata)
        if result:
            return result
        else:
            result = betterthannothing(somedata)
            if result:
                return result
            else:
                return None

这看起来真不怎么样。当然,有些人喜欢省略一些语法:

def analyse(somedata):
    result = bestapproach(somedata)
    if result:
        return result
    result = notasgood(somedata)
    if result:
        return result
    result = betterthannothing(somedata)
    if result:
        return result

但这样也没什么好转,代码里还是有很多重复的部分,而且依然很丑。

我尝试使用内置的 iter() 和一个哨兵值,但在这种情况下,None 值是用来表示循环应该继续,而不是用来表示循环应该结束的哨兵。

在Python中,还有没有其他(合理的)方法可以实现这种“继续尝试直到找到有效结果”的模式呢?

我需要说明的是,“返回值满足某个条件”并不局限于条件是 if bool(result) is True 这种情况。可能是一些分析函数生成的列表,每个函数都产生一个系数来衡量成功的程度(比如R平方值),你可能想设定一个最低接受标准。因此,一个通用的解决方案不应该仅仅依赖于结果的真假值。

3 个回答

3

这段代码很符合Python的风格:

result = (i for i in (f(somedata) for f in funcs) if i is not None).next()

这里的思路是使用生成器,这样可以实现“懒惰求值”,也就是说不是一次性计算所有的函数。你可以随意修改conditionfuncs,所以这个方法比Grapsus提出的or方案更灵活。

这就是为什么生成器在Python中非常强大的一个好例子。

下面是更详细的解释:

我们向这个生成器请求一个元素。外层生成器会向内层生成器(f(d) for f in funcs)请求一个元素,并进行计算。如果这个元素符合条件,那么就结束了;如果不符合,就继续向内层生成器请求下一个元素。

6

如果函数的数量不多,为什么不直接用 or 操作符呢?

d = 'somedata'
result = f1(d) or f2(d) or f3(d) or f4(d)

这样做的话,只会执行这些函数,直到其中一个函数返回的结果不是 False 为止。

9

选项 #1: 使用 or

当你知道总共有多少个函数,并且这个数量不多时,如果测试条件完全依赖于返回值的真假,可以像Grapsus建议的那样简单地使用 or

d = 'somedata'
result = f1(d) or f2(d) or f3(d) or f4(d)

因为Python的布尔运算符是短路运算,所以函数会从右到左执行,直到其中一个函数返回的值被评估为 True,这时就把这个值赋给 result,剩下的函数就不再执行;或者直到所有函数都执行完,这时 result 会被赋值为 False


选项 #2: 使用生成器

当你不知道总共有多少个函数,或者函数数量非常多时,可以使用一行代码的生成器表达式,正如Bitwise所建议的那样:

result = (r for r in (f(somedata) for f in functions) if <test-condition>).next()

这个方法比选项 #1 多了一个好处,就是你可以使用任何你想要的 <test-condition>,而不仅仅依赖于真假值。每次调用 .next() 时:

  1. 我们向外部生成器请求下一个元素
  2. 外部生成器向内部生成器请求下一个元素
  3. 内部生成器从 functions 中请求一个 f,并尝试计算 f(somedata)
  4. 如果这个表达式可以计算(也就是说,f 是一个函数,somedata 是一个有效的参数),内部生成器就会把 f(somedata) 的返回值传给外部生成器
  5. 如果 <test-condition> 满足,外部生成器就会把 f(somedata) 的返回值传递给 result
  6. 如果在第5步中 <test-condition> 不满足,就重复第2到第4步

这个方法的一个缺点是,嵌套的生成器表达式可能不如多行代码直观。此外,如果内部生成器在满足测试条件之前就耗尽了,调用 .next() 会引发 StopIteration 异常,这需要处理(在try-except块中)或者避免(确保最后一个函数总是“成功”)。


选项 #3: 使用自定义函数

因为我们可以把可调用的函数放在一个列表中,所以一个选择是明确列出你想要“尝试”的函数,并按照它们应该被使用的顺序进行迭代:

def analyse(somedata):
    analysis_functions = [best, okay, poor]
    for f in analysis_functions:
        result = f(somedata)
        if result:
            return result

优点:解决了重复代码的问题,更清楚地表明你正在进行一个迭代过程,并且在找到一个“好”的结果后不会继续执行后面的函数。

这也可以用Python的 for ... else 语法来写:*

def analyse(somedata):
    analysis_functions = [best, okay, poor]
    for f in analysis_functions:
        result = f(somedata)
        if result:
            break
    else:
        return None
    return result

这里的好处是可以识别出不同的退出方式,这在你希望 analyse() 函数完全失败时返回其他值而不是 None,或者引发异常时会很有用。否则,这种写法只是更长且更复杂。

*如在“将代码转变为优雅、符合习惯的Python”中描述,开始于15:50。

撰写回答