避免“ if x:return x”语句的Python方法


218

我有一个方法可以依次调用其他4种方法来检查特定条件,并且每当一个方法返回Truthy时立即返回(不检查以下方法)。

def check_all_conditions():
    x = check_size()
    if x:
        return x

    x = check_color()
    if x:
        return x

    x = check_tone()
    if x:
        return x

    x = check_flavor()
    if x:
        return x
    return None

这似乎是很多行李代码。与其执行每行2行的if语句,不如执行以下操作:

x and return x

但这是无效的Python。我在这里错过了一个简单,优雅的解决方案吗?顺便说一句,在这种情况下,这四种检查方法可能很昂贵,因此我不想多次调用它们。


7
这些x是多少?它们只是对/错,还是包含某些信息的数据结构,其中无或类似情况被用作特殊情况来指示缺少任何数据?如果是后者,则几乎应该肯定会使用异常。
纳撒尼尔(Nathaniel)

13
@gerrit上面显示的代码是假设/伪代码,在代码审阅中不合时宜。如果该帖子的作者希望对其真实的,实际的工作代码进行审核,则可以,他们欢迎在Code Review上发布。
Phrancis '16

4
你为什么认为x and return xif x: return x呢?后者更具可读性,因此可以维护。您不必太担心字符或行的数量;可读性很重要。无论如何,它们都是完全相同数量的非空白字符,如果确实需要,它们if x: return x只能在一行上工作。
marcelm '16

3
请说明您是否关心实际值,还是只需要返回一个布尔值。这使哪些选项可用以及哪些选项更清楚地传达了意图有所不同。命名表明您只需要一个布尔值。避免多次调用这些功能是否重要也有区别。函数是否采用任何或不同的参数集也可能很重要。如果没有这些澄清,我认为这个问题属于“不清楚”,“过于广泛”或“基于意见”之一。
jpmc26 2013年

7
@ jpmc26 OP显式地讲到真实的返回值,然后他的代码返回x(而不是bool(x)),按目前的情况,我认为可以安全地假设OP的函数可以返回任何东西,而他想要第一个真实的东西。
timgeb

Answers:


278

您可以使用循环:

conditions = (check_size, check_color, check_tone, check_flavor)
for condition in conditions:
    result = condition()
    if result:
        return result

这具有额外的优势,您现在可以使条件数量可变。

您可以使用map()+ filter()(Python 3版本,使用Python 2中的future_builtins版本)来获取第一个这样的匹配值:

try:
    # Python 2
    from future_builtins import map, filter
except ImportError:
    # Python 3
    pass

conditions = (check_size, check_color, check_tone, check_flavor)
return next(filter(None, map(lambda f: f(), conditions)), None)

但是,如果更具可读性,则值得商bat。

另一个选择是使用生成器表达式:

conditions = (check_size, check_color, check_tone, check_flavor)
checks = (condition() for condition in conditions)
return next((check for check in checks if check), None)

27
如果条件实际上只是条件,即布尔值,那么在您的第一个建议中,您还可以使用内置函数any而不是循环。return any(condition() for condition in conditions)

4
@Leonhard:any内部几乎具有相同的实现。但是它看起来好多了,请发布答案作为答案)
Nick Volynkin

13
可读性胜过几乎所有其他考虑。你说,地图/过滤器是“值得商bat的”,我投赞成票的理由是丑陋的。的确,谢谢,但是如果我的团队中有人为此代码放置了地图/过滤器,我会把他们转移到另一个团队或分配他们的便盆工作。
凯文·赖斯

15
这个无法读取的代码块真的是“ pythonian”吗?特别是压缩的想法conditionsarguments?恕我直言,这比原始代码差很多,原始代码需要我的大脑分析器解析大约10秒。
2016年

34
他们说,“更喜欢Python”。他们说:“ Perl不可读”。然后,这发生了:return next((check for check in checks if check), None)
jja

393

除了Martijn的好答案之外,您还可以连锁or。这将返回第一个真实值,或者None如果没有真实值,则返回:

def check_all_conditions():
    return check_size() or check_color() or check_tone() or check_flavor() or None

演示:

>>> x = [] or 0 or {} or -1 or None
>>> x
-1
>>> x = [] or 0 or {} or '' or None
>>> x is None
True

9
当然可以,但是如果有很多选择的话,那么快速阅读将很繁琐。另外,我的方法可让您使用多种条件。
马丁·彼得斯

14
您可以使用@MartijnPieters \将每张支票放在自己的行上。
Caridorc

12
@MartijnPieters我从未暗示过我的答案会比您的好,我也喜欢您的答案:)
timgeb 16-3-20

38
@Caridorc:我非常不喜欢使用\扩展逻辑线。尽可能使用括号;因此return (....),根据需要插入换行符。不过,这将是一条长长的逻辑线。
马丁·皮特斯

47
我认为这是更好的解决方案。论点“如果有多个选择,它将变得乏味[..]”是没有意义的,因为单个函数无论如何都不应进行过多的检查。如果需要,应将检查分为多个功能。
BlueRaja-Danny Pflughoeft

88

不要改变

如其他答案所示,还有其他方法可以做到这一点。没有一个像您的原始代码一样清晰。


39
我对此表示反对,但您的建议是可以提出的合理建议。就个人而言,例如,当timgeb的解决方案立即单击时,我的眼睛在尝试阅读OP时会感到疲劳。
Reti43 '16

3
这确实是一个见解的问题。我个人而言,我会删除换行符:,因为我认为if x: return x还不错,它使函数看起来更紧凑。但这可能只是我。
2016年

2
不只是你 or像使用timgeb那样使用是正确且易于理解的习惯用法。许多语言都有这个。也许当它被称为orelse是更清楚,但即使是普通的旧or(或||其他语言)的意思可以理解为替代尝试,如果第一个“不工作”。
Ray Toal

1
@RayToal:从其他语言导入成语是混淆代码的好方法。
杰克·艾德利

1
有时肯定可以!这也可以是一种开放思想并引导人们发现前所未有的新模式和范例的方法。人们通过借阅,共享和尝试新事物来发展风格。双向均可。无论如何,我从来没有听说过使用or标记的非Python语言或以任何方式进行混淆,但这仍然是一个意见问题-应该如此。
Ray Toal

83

实际上,与timgeb的答案相同,但是您可以使用括号进行更好的格式化:

def check_all_the_things():
    return (
        one()
        or two()
        or five()
        or three()
        or None
    )

8
大家请帮助将此答案提高到第一名。尽你所能!
Gyom '16

74

根据Curly的定律,可以通过分解两个方面来使此代码更具可读性:

  • 我要检查什么?
  • 有一件事情返回真吗?

分为两个功能:

def all_conditions():
    yield check_size()
    yield check_color()
    yield check_tone()
    yield check_flavor()

def check_all_conditions():
    for condition in all_conditions():
        if condition:
            return condition
    return None

这样可以避免:

  • 复杂的逻辑结构
  • 真的很长
  • 重复

...同时保持线性,易于阅读的流程。

根据您的特定情况,您可能还会想出更好的函数名称,从而使其更具可读性。


我喜欢这一点,尽管应该将True / False更改为condition / None以匹配该问题。
马尔科姆

2
这是我最喜欢的!它也应付不同的检查和论点。对于这个特定的例子,它可能过度设计了,但是对于将来的问题却是一个非常有用的工具!
rjh

4
请注意,这return None不是必需的,因为函数None默认返回。但是,None显式返回没有任何问题,我喜欢您选择这样做。
timgeb

1
我认为使用局部函数定义可以更好地实现此方法。
杰克·艾德利

1
@timgeb“显式比隐式更好” ,Python的Zen
jpmc26,2016年

42

这是Martijns第一个示例的变体。它还使用“可调用集合”样式以允许发生短路。

可以使用内置功能来代替循环any

conditions = (check_size, check_color, check_tone, check_flavor)
return any(condition() for condition in conditions) 

请注意,此方法any返回一个布尔值,因此,如果您需要支票的确切返回值,则此解决方案将不起作用。any不会区分14'red''sharp''spicy'作为返回值,将他们全部返回True


您可以next(itertools.ifilter(None, (c() for c in conditions)))获取实际值而无需将其转换为布尔值。
kojiro

1
是否any实际上短路?
zwol

1
@zwol是,可以尝试使用一些示例函数,或参见docs.python.org/3/library/functions.html

1
这比将4个函数与'或'链接起来的可读性差,并且只有在条件数量很大或动态的情况下才有回报。
rjh

1
@rjh完全可读;这只是列表文字和理解。我更喜欢它,因为大约第三次后我的眼睛就闪了x = bar(); if x: return x;

27

您是否考虑过只写if x: return x一行?

def check_all_conditions():
    x = check_size()
    if x: return x

    x = check_color()
    if x: return x

    x = check_tone()
    if x: return x

    x = check_flavor()
    if x: return x

    return None

这与您所做的一样,没有什么重复,但是IMNSHO它的读起来相当流畅。


24

我很惊讶没有人提到any为此目的而设计的内置功能:

def check_all_conditions():
    return any([
        check_size(),
        check_color(),
        check_tone(),
        check_flavor()
    ])

请注意,尽管此实现可能是最清晰的,但即使第一个是,它也会评估所有检查True


如果您确实需要在第一次失败的检查时停止,请考虑使用使用reduce哪个将列表转换为简单值:

def check_all_conditions():
    checks = [check_size, check_color, check_tone, check_flavor]
    return reduce(lambda a, f: a or f(), checks, False)

reduce(function, iterable[, initializer]):将两个参数的函数从左到右累计应用于iterable的项目,以将iterable减少为单个值。左参数x是累加值,右参数y是可迭代对象的更新值。如果存在可选的初始化程序,则将其放置在计算中可迭代项的前面

在您的情况下:

  • lambda a, f: a or f()是用于检查累加器a或当前检查f()是否为的函数True。请注意,如果aTruef()则不会进行评估。
  • checks包含检查功能(flambda中的项目)
  • False 是初始值,否则将不进行检查并且结果始终为 True

anyreduce对于函数式编程的基本工具。我强烈建议您训练这些技巧,以及map哪些也很棒!


9
any仅在检查实际上返回布尔值(字面值True或)的情况下有效False,但问题未指定该值。您需要使用reduce返回支票返回的实际值。同样,避免any使用生成器(例如)评估所有检查非常容易any(c() for c in (check_size, check_color, check_tone, check_flavor))。正如莱昂哈德的答案
大卫ž

我喜欢您对的解释和用法reduce。像@DavidZ一样,我认为您的解决方案any应使用生成器,需要指出的是,它仅限于返回TrueFalse
timgeb

1
@DavidZ实际上any与truthy价值工程:any([1, "abc", False]) == Trueany(["", 0]) == False
ngasull

3
@blint抱歉,我不清楚。问题的目的是返回检查结果(而不仅仅是指示检查成功还是失败)。我指出,any只适用于如果实际布尔值从检查函数返回的目的。
David Z

19

如果您想要相同的代码结构,则可以使用三元语句!

def check_all_conditions():
    x = check_size()
    x = x if x else check_color()
    x = x if x else check_tone()
    x = x if x else check_flavor()

    return x if x else None

我认为,如果您仔细看,这看起来很好看。

演示:

正在运行的屏幕截图


7
终端上方的小ASCII鱼怎么办?

36
@LegoStormtroopr我用鱼壳,所以我用ascii鱼缸装饰它,让我高兴。:)
Phinet

3
感谢您的好鱼(顺便说一下颜色,那是什么编辑器?)
mathreadler

4
您可以在fishshell.com上找到鱼,这里的ascii的配置文件位于pastebin.com/yYVYvVeK,编辑器也是崇高的文字。
Phinet '16

9
x if x else <something>可以简化为x or <something>

5

对我来说,最好的答案是@ phil-frost,其次是@ wayne-werner。

我发现有趣的是,没有人说过一个函数将返回许多不同数据类型这一事实,这将使然后必须对x本身的类型进行检查以进行进一步的工作。

因此,我将@PhilFrost的响应与保持单一类型的想法混合在一起:

def all_conditions(x):
    yield check_size(x)
    yield check_color(x)
    yield check_tone(x)
    yield check_flavor(x)

def assessed_x(x,func=all_conditions):
    for condition in func(x):
        if condition:
            return x
    return None

请注意,x它作为参数传递,但也all_conditions用作检查函数的传递生成器,在此函数中,所有函数都x将被检查,然后返回TrueFalse。通过使用funcwith all_conditions作为默认值,您可以使用assessed_x(x),也可以通过传递进一步的个性化生成器func

这样一来,您x就可以通过一次支票,但是它将始终是同一类型。


4

理想情况下,我将重写check_ 函数以返回True或返回False值。然后您的支票变成

if check_size(x):
    return x
#etc

假设您x不是一成不变的,则您的函数仍可以对其进行修改(尽管他们不能重新分配它)-但是一个被调用的函数check实际上不应进行任何修改。


3

上面的Martijns第一个示例略有变化,避免了if循环内:

Status = None
for c in [check_size, check_color, check_tone, check_flavor]:
  Status = Status or c();
return Status

可以?您仍在进行比较。在您的版本中,您还将检查所有条件,无论这些条件是否成立,并且在第一个真实值的实例上都不返回,这取决于这些函数的成本,这可能不是所希望的。
Reti43 '16

4
@ Reti43:如果是真的,Status or c()将跳过/短循环评估对调用的判断,因此此答案中的代码似乎没有调用比OP的代码更多的函数。stackoverflow.com/questions/2580136/…–c()Status
尼尔·斯莱特

2
@NeilSlater是的。我看到的唯一缺点是,最好的情况是在O(n)中,因为当第一个函数在O(1)中返回真值时,listiterator必须屈服n次(当它是O(1)时)。
timgeb

1
是的好点。我只希望希望c()比循环几乎为空的循环花费更多的时间。如果味道不错,检查风味至少需要整个晚上。
mathreadler '16

3

我喜欢@timgeb。在此期间,我想补充一点,表达Nonereturn语句没有必要为收集or分离的语句进行评估,并在第一无为零,无空,则返回无-无,如果没有任何然后None返回是否有None

所以我的check_all_conditions()函数看起来像这样:

def check_all_conditions():
    return check_size() or check_color() or check_tone() or check_flavor()

使用timeitwith时,number=10**7我查看了一些建议的运行时间。为了进行比较,我仅使用该random.random()函数返回字符串或None基于随机数。这是完整的代码:

import random
import timeit

def check_size():
    if random.random() < 0.25: return "BIG"

def check_color():
    if random.random() < 0.25: return "RED"

def check_tone():
    if random.random() < 0.25: return "SOFT"

def check_flavor():
    if random.random() < 0.25: return "SWEET"

def check_all_conditions_Bernard():
    x = check_size()
    if x:
        return x

    x = check_color()
    if x:
        return x

    x = check_tone()
    if x:
        return x

    x = check_flavor()
    if x:
        return x
    return None

def check_all_Martijn_Pieters():
    conditions = (check_size, check_color, check_tone, check_flavor)
    for condition in conditions:
        result = condition()
        if result:
            return result

def check_all_conditions_timgeb():
    return check_size() or check_color() or check_tone() or check_flavor() or None

def check_all_conditions_Reza():
    return check_size() or check_color() or check_tone() or check_flavor()

def check_all_conditions_Phinet():
    x = check_size()
    x = x if x else check_color()
    x = x if x else check_tone()
    x = x if x else check_flavor()

    return x if x else None

def all_conditions():
    yield check_size()
    yield check_color()
    yield check_tone()
    yield check_flavor()

def check_all_conditions_Phil_Frost():
    for condition in all_conditions():
        if condition:
            return condition

def main():
    num = 10000000
    random.seed(20)
    print("Bernard:", timeit.timeit('check_all_conditions_Bernard()', 'from __main__ import check_all_conditions_Bernard', number=num))
    random.seed(20)
    print("Martijn Pieters:", timeit.timeit('check_all_Martijn_Pieters()', 'from __main__ import check_all_Martijn_Pieters', number=num))
    random.seed(20)
    print("timgeb:", timeit.timeit('check_all_conditions_timgeb()', 'from __main__ import check_all_conditions_timgeb', number=num))
    random.seed(20)
    print("Reza:", timeit.timeit('check_all_conditions_Reza()', 'from __main__ import check_all_conditions_Reza', number=num))
    random.seed(20)
    print("Phinet:", timeit.timeit('check_all_conditions_Phinet()', 'from __main__ import check_all_conditions_Phinet', number=num))
    random.seed(20)
    print("Phil Frost:", timeit.timeit('check_all_conditions_Phil_Frost()', 'from __main__ import check_all_conditions_Phil_Frost', number=num))

if __name__ == '__main__':
    main()

结果如下:

Bernard: 7.398444877040768
Martijn Pieters: 8.506569201346597
timgeb: 7.244275416364456
Reza: 6.982133448743038
Phinet: 7.925932800076634
Phil Frost: 11.924794811353031

2

这种方法有点开箱即用,但是我认为最终结果很简单,易读并且看起来不错。

基本思想是raise当其中一个函数的评估结果为“真”并返回结果时出现异常。这是它的外观:

def check_conditions():
    try:
        assertFalsey(
            check_size,
            check_color,
            check_tone,
            check_flavor)
    except TruthyException as e:
        return e.trigger
    else:
        return None

assertFalsey当一个调用的函数参数评估为真时,您将需要一个引发异常的函数:

def assertFalsey(*funcs):
    for f in funcs:
        o = f()
        if o:
            raise TruthyException(o)

可以修改上述内容,以便也为要评估的功能提供参数。

当然,您将需要TruthyException自身。此异常提供了object触发异常的:

class TruthyException(Exception):
    def __init__(self, obj, *args):
        super().__init__(*args)
        self.trigger = obj

当然,您可以将原始功能转换为更通用的功能:

def get_truthy_condition(*conditions):
    try:
        assertFalsey(*conditions)
    except TruthyException as e:
        return e.trigger
    else:
        return None

result = get_truthy_condition(check_size, check_color, check_tone, check_flavor)

这可能会慢一些,因为您同时使用了一条if语句和一个异常。但是,该异常最多只能处理一次,因此对性能的影响应该很小,除非您希望运行检查并获得True成千上万次的值。


//,可爱!对此类事情使用异常处理是否被认为是“ Pythonic”?
弥敦道(Nathan Basanese)'16

@NathanBasanese当然,异常始终用于控制流。StopIteration这是一个很好的例子:每次耗尽可迭代对象时都会引发异常。您要避免的事情是一次又一次地引发异常,这会增加代价。但是一次却不是。
瑞克(Rick)

//啊,我把它你指的是像programmers.stackexchange.com/questions/112463/...。我对这个问题和这个答案投了赞成票。我认为这是Python 3文档:docs.python.org/3/library/stdtypes.html#iterator-types
弥敦道(Nathan Basanese)'16

1
您想定义一个通用函数和一个例外,只是为了在某个地方对其他函数进行一些检查?我觉得有点多。
Blacklight Shining

@BacklightShining我同意。我绝对不会亲自做这个。OP要求避免重复代码的方法,但我认为他开始时做得很好。
瑞克(Rick)

2

pythonic方式是使用reduce(如已经提到的)或itertools(如下所示),但是在我看来,仅使用or运算符的短路会产生更清晰的代码

from itertools import imap, dropwhile

def check_all_conditions():
    conditions = (check_size,\
        check_color,\
        check_tone,\
        check_flavor)
    results_gen = dropwhile(lambda x:not x, imap(lambda check:check(), conditions))
    try:
        return results_gen.next()
    except StopIteration:
        return None

0

我要跳进这里,从来没有写过Python的任何一行,但是我认为这if x = check_something(): return x是有效的吗?

如果是这样的话:

def check_all_conditions():

    if (x := check_size()): return x
    if (x := check_color()): return x
    if (x := check_tone()): return x
    if (x := check_flavor()): return x

    return None

1
这不是有效的Python,不。Python不允许您那样使用赋值运算符。但是,最近添加了一个新的特殊赋值表达式,因此您现在可以编写if ( x := check_size() ) :以达到相同的效果。
杰克·艾德利

0

或使用max

def check_all_conditions():
    return max(check_size(), check_color(), check_tone(), check_flavor()) or None

-2

过去,我见过一些用dicts进行switch / case语句的有趣实现,这使我想到了这个答案。使用您提供的示例,您将获得以下内容。(这很疯狂using_complete_sentences_for_function_names,因此check_all_conditions将其重命名为status。请参阅(1))

def status(k = 'a', s = {'a':'b','b':'c','c':'d','d':None}) :
  select = lambda next, test : test if test else next
  d = {'a': lambda : select(s['a'], check_size()  ),
       'b': lambda : select(s['b'], check_color() ),
       'c': lambda : select(s['c'], check_tone()  ),
       'd': lambda : select(s['d'], check_flavor())}
  while k in d : k = d[k]()
  return k

select函数消除了每次调用check_FUNCTION两次的需要,即避免check_FUNCTION() if check_FUNCTION() else next添加另一个函数层。这对于长时间运行的功能很有用。字典中的lambda会延迟其值的执行,直到while循环为止。

作为奖励,您可以修改执行顺序,甚至可以通过更改k和跳过某些测试,s例如k='c',s={'c':'b','b':None}减少测试数量)和颠倒原始处理顺序。

timeit研究员可能讨价还价增加额外的一层或两层堆栈,为字典成本抬头,但你似乎更关心的是代码的可爱的成本。

另外,一个更简单的实现可能如下:

def status(k=check_size) :
  select = lambda next, test : test if test else next
  d = {check_size  : lambda : select(check_color,  check_size()  ),
       check_color : lambda : select(check_tone,   check_color() ),
       check_tone  : lambda : select(check_flavor, check_tone()  ),
       check_flavor: lambda : select(None,         check_flavor())}
  while k in d : k = d[k]()
  return k
  1. 我的意思不是用pep8而是用一个简洁的描述性词代替句子。授予OP可能遵循某些编码约定,使用一些现有代码库或不在乎其代码库中的简洁术语。

1
有时候,一个词会用时,人们就会对他们的命名感到疯狂。以OP的代码为例,他不太可能调用函数,check_no/some/even/prime/every_third/fancy_conditions而仅调用此函数,那么为什么不调用它status或坚持呢check_status?使用_all_是多余的,他并不能确保宇宙的完整性。命名应尽可能使用一组一致的关键字,以利用名称间隔。冗长的句子最好用作文档字符串。一个人很少需要超过8-10个字符来简洁地描述某些内容。
卡雷尔

1
我喜欢长函数名,因为我希望更高级的函数能够自我记录。但这check_all_conditions是一个不好的名字,因为它不会检查所有条件是否为真。我会用matches_any_condition
John Hazen

这是一个有趣的策略。我试图将以后要打错的字母的数量减到最少:)似乎在我确实想提供有用的提示时,在我的解决方案中提出了很多意见。是否应将其删除?
卡雷尔

2
这似乎太过分了,尤其是考虑这个问题的其他解决方案时。OP试图做的事情一点都不复杂。解决方案应该足够简单,以了解半睡。而且我不知道这是怎么回事。
Blacklight Shining

我的目标是灵活性。修改后的答案以包含较少的“ hacky”变体
Carel
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.