我该如何调用一系列函数直到返回值满足某个条件?
有时候我发现自己写的代码像这样:
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 个回答
这段代码很符合Python的风格:
result = (i for i in (f(somedata) for f in funcs) if i is not None).next()
这里的思路是使用生成器,这样可以实现“懒惰求值”,也就是说不是一次性计算所有的函数。你可以随意修改condition
和funcs
,所以这个方法比Grapsus提出的or
方案更灵活。
这就是为什么生成器在Python中非常强大的一个好例子。
下面是更详细的解释:
我们向这个生成器请求一个元素。外层生成器会向内层生成器(f(d) for f in funcs)
请求一个元素,并进行计算。如果这个元素符合条件,那么就结束了;如果不符合,就继续向内层生成器请求下一个元素。
如果函数的数量不多,为什么不直接用 or
操作符呢?
d = 'somedata'
result = f1(d) or f2(d) or f3(d) or f4(d)
这样做的话,只会执行这些函数,直到其中一个函数返回的结果不是 False
为止。
选项 #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()
时:
- 我们向外部生成器请求下一个元素
- 外部生成器向内部生成器请求下一个元素
- 内部生成器从
functions
中请求一个f
,并尝试计算f(somedata)
- 如果这个表达式可以计算(也就是说,
f
是一个函数,somedata
是一个有效的参数),内部生成器就会把f(somedata)
的返回值传给外部生成器 - 如果
<test-condition>
满足,外部生成器就会把f(somedata)
的返回值传递给result
- 如果在第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。