【问题标题】:Pythonic way to avoid "if x: return x" statements避免“if x: return x”语句的 Pythonic 方法
【发布时间】:2016-07-07 04:01:21
【问题描述】:

我有一个方法,它依次调用其他 4 个方法来检查特定条件,并在返回真值时立即返回(不检查以下那些)。

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。我在这里错过了一个简单、优雅的解决方案吗?顺便说一句,在这种情况下,这四种检查方法可能很昂贵,所以我不想多次调用它们。

【问题讨论】:

  • 这些 x 是什么?它们只是 True/False,还是它们是包含一些信息的数据结构,而 None 或类似的被用作表示没有任何数据的特例?如果是后者,您几乎肯定应该使用异常。
  • @gerrit 上面显示的代码是假设/伪代码,与代码审查无关。如果帖子的作者希望他们的真实、实际的工作代码得到审查,那么是的,欢迎他们在 Code Review 上发帖。
  • 为什么你认为x and return xif x: return x 好?后者更具可读性,因此可维护。您不必太担心字符或行数;可读性很重要。无论如何,它们是完全相同数量的非空白字符,如果你真的需要,if x: return x 可以在一行上正常工作。
  • 请澄清您是否关心实际值,或者您真的只需要返回一个布尔值。这会有所不同,哪些选项可用,哪些选项更清楚地传达了意图。命名表明您只需要一个布尔值。避免多次调用这些函数是否重要也很重要。函数是否采用任何或不同的参数集也可能很重要。如果没有这些澄清,我认为这个问题属于不清楚、过于宽泛或基于意见的问题之一。
  • @jpmc26 OP 明确谈到了真实的返回值,然后他的代码返回 x(而不是 bool(x))所以就目前而言,我认为假设 OP 的函数可以返回是安全的任何东西,他想要第一个真实的东西。

标签: python if-statement


【解决方案1】:

你可以使用循环:

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 versions)来获取第一个这样的匹配值:

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)

但如果这更具可读性是值得商榷的。

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

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)

【讨论】:

  • 如果条件真的只是条件,即布尔值,那么在您的第一个提案中,您还可以使用内置的 any 而不是循环。 return any(condition() for condition in conditions)
  • @Leonhard: any 内部有几乎相同的实现。但它看起来好多了,请作为答案发布)
  • 可读性胜过几乎所有其他考虑因素。你说,地图/过滤器是“有争议的”,我把票投给了无可争议的丑陋。谢谢,当然,但是如果我团队中的任何人为此代码添加了地图/过滤器,我会将他们转移到另一个团队或分配他们到便盆职责。
  • 这段不可读的代码真的是“pythonian”吗?尤其是压缩conditionsarguments 的想法?恕我直言,这比原始代码差得多,我的大脑解析器需要大约 10 秒才能解析。
  • “更喜欢 Python”,他们说。 “Perl 是不可读的”,他们说。然后这件事发生了:return next((check for check in checks if check), None).
【解决方案2】:

除了 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

【讨论】:

  • 当然,但是如果有多个选项,快速阅读会变得乏味。另外,我的方法允许您使用可变数量的条件。
  • @MartijnPieters 您可以使用\ 将每张支票放在自己的行上。
  • @MartijnPieters 我从来没有暗示我的答案比你的好,我也喜欢你的答案:)
  • @Caridorc:我非常不喜欢使用\ 来扩展逻辑行。尽可能使用括号;所以return (....) 根据需要插入换行符。不过,这将是一条很长的逻辑线路。
  • 我认为这是更好的解决方案。论点 “如果有多个选项,它将变得乏味 [..]” 没有实际意义,因为无论如何单个函数都不应该进行过多的检查。如果需要,应将检查拆分为多个功能。
【解决方案3】:

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

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

如果你看一下,我认为这看起来不错而且清晰。

演示:

【讨论】:

  • 你的终端上方的小 ASCII 鱼是怎么回事?
  • @LegoStormtroopr 我用的是鱼壳,所以我用 ascii 鱼缸装饰它让我开心。 :)
  • 谢谢你的好鱼(顺便说一下颜色,那是什么编辑器?)
  • 你可以在fishshell.com得到fish,ascii的配置文件在这里pastebin.com/yYVYvVeK,编辑器也是sublime text。
  • x if x else <something> 可以简化为x or <something>
【解决方案4】:

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

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

【讨论】:

  • 是吗?你还是做个比较吧。在您的版本中,您还将检查所有条件,而不是在第一个真实值实例时返回,这取决于这些函数的成本,这可能是不可取的。
  • @Reti43: Status or c() 将跳过/short-circui 评估对 c() 的调用,如果 Status 是真实的,所以这个答案中的代码似乎没有调用比 OP 更多的函数代码。 stackoverflow.com/questions/2580136/…
  • @NeilSlater 是的。我看到的唯一缺点是现在最好的情况是在 O(n) 中,因为如果第一个函数在 O(1) 中返回一些真实的东西,那么 listiterator 必须产生 n 次,而之前是 O(1)。
  • 是的,好点。我只希望 c() 比循环一个几乎空的循环需要更多的时间来评估。如果味道好的话,至少要花一整个晚上的时间来检查味道。
【解决方案5】:

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

您可以使用内置的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))) 获取实际值,而无需将其转换为布尔值。
  • any 真的短路了吗?
  • @zwol 是的,用一些示例函数试试看,或者查看docs.python.org/3/library/functions.html
  • 这比用 'or' 链接 4 个函数的可读性差,并且只有在条件数量很大或动态的情况下才会得到回报。
  • @rjh 可读性很好;它只是一个列表文字和理解。我更喜欢它,因为我的眼睛在第三次之后就呆滞了x = bar(); if x: return x;
【解决方案6】:

不要改变它

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

【讨论】:

  • 我反对这一点,但您的建议是合理的。就个人而言,我发现我的眼睛在试图阅读 OP 时感到紧张,例如,timgeb 的解决方案会立即点击。
  • 这真是见仁见智。就我个人而言,我会删除: 之后的换行符,因为我认为if x: return x 非常好,它使函数看起来更紧凑。但这可能只是我。
  • 不只是你。像 timgeb 那样使用or 是一个正确且易于理解的习语。许多语言都有这个;也许当它被称为orelse 时它会更清楚,但即使是普通的旧or(或其他语言中的||意味着被理解为第一个尝试的替代方案一个“不起作用”。
  • @RayToal:从其他语言导入成语是混淆代码的好方法。
  • 有时是的,当然!它也可以成为一种开放思想并引导人们发现以前没有人尝试过的新的更好的模式和范式的方式。风格是通过人们借用、分享和尝试新事物而演变的。双向工作。无论如何,我从未听说过使用or 标记为非 Pythonic 或以任何方式混淆,但这无论如何都是一个意见问题——应该如此。
【解决方案7】:

根据Curly's law,您可以通过拆分两个关注点来使这段代码更具可读性:

  • 我要检查什么?
  • 一件事是否返回了真实?

分为两个功能:

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来匹配问题。
  • 这是我的最爱!它也可以处理不同的检查和参数。对于这个特定的例子来说可能设计过度了,但对于未来的问题来说是一个非常有用的工具!
  • 请注意return None 不是必需的,因为函数默认返回None。但是,明确返回 None 并没有错,我喜欢你选择这样做。
  • 我认为这种方法最好用本地函数定义来实现。
  • @timgeb “显式优于隐式”,Zen of Python
【解决方案8】:

我很惊讶没有人提到为此目的而制作的内置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]) : 应用两个函数 参数从左到右累积到可迭代项, 以便将可迭代减少为单个值。左边的参数,x, 是累积值,正确的参数 y 是更新 来自可迭代的值。如果存在可选的初始化器,它是 放在计算中可迭代项之前

在你的情况下:

  • lambda a, f: a or f() 是检查累加器 a 或当前检查 f() 是否为 True 的函数。请注意,如果 aTrue,则不会评估 f()
  • checks 包含检查函数(来自 lambda 的 f 项)
  • False 是初始值,否则不会发生检查,结果将始终为 True

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

【讨论】:

  • any 仅在检查实际返回布尔值时才有效,即TrueFalse,但问题并未具体说明。您需要使用 reduce 来返回支票返回的实际值。此外,很容易避免使用生成器评估 any 的所有检查,例如any(c() for c in (check_size, check_color, check_tone, check_flavor))。如Leonhard's answer
  • 我喜欢你对reduce的解释和使用。像@DavidZ,我相信您使用any 的解决方案应该使用生成器,并且需要指出它仅限于返回TrueFalse
  • @DavidZ 实际上 any 使用真实值:any([1, "abc", False]) == Trueany(["", 0]) == False
  • @blint 抱歉,我不清楚。该问题的目标是返回检查结果(而不仅仅是指示检查是成功还是失败)。我指出any 仅在检查函数返回实际布尔值时才用于那个目的。
【解决方案9】:

您是否考虑过将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 它读起来更流畅。

【讨论】:

    【解决方案10】:

    理想情况下,我会重写 check_ 函数以返回 TrueFalse 而不是一个值。然后你的支票就变成了

    if check_size(x):
        return x
    #etc
    

    假设你的 x 不是不可变的,你的函数仍然可以修改它(尽管他们不能重新分配它) - 但是一个名为 check 的函数无论如何都不应该真正修改它。

    【讨论】:

      【解决方案11】:

      实际上与 timgeb 的答案相同,但您可以使用括号来获得更好的格式:

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

      【讨论】:

      • 请大家帮忙把这个答案提高到第一名。尽你的本分!
      【解决方案12】:

      过去,我已经看到了一些有趣的 switch/case 语句与 dicts 的实现,这些实现让我得到了这个答案。使用您提供的示例,您将获得以下信息。 (疯了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
      

      选择函数消除了调用每个check_FUNCTION 两次的需要,即通过添加另一个函数层来避免check_FUNCTION() if check_FUNCTION() else next。这对于长时间运行的函数很有用。 dict 中的 lambda 会将其值的执行延迟到 while 循环。

      作为奖励,您可以修改执行顺序,甚至通过更改 ks 来跳过一些测试,例如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 可能遵循一些编码约定,使用一些现有的代码库或不关心其代码库中的简洁术语。

      【讨论】:

      • 有时人们会为自己的命名而疯狂,而一个词就可以了。以 OP 的代码为例,他不太可能拥有称为 check_no/some/even/prime/every_third/fancy_conditions 的函数,但只有这一个函数,所以为什么不将其称为 status 或者如果有人坚持使用 check_status。使用_all_ 是多余的,他并不能确保宇宙的完整性。命名肯定应该使用一组一致的关键字,尽可能利用名称间距。冗长的句子最好用作文档字符串。一个人很少需要超过 8-10 个字符来简洁地描述某事。
      • 我喜欢长函数名,因为我希望更高级别的函数能够自我记录。但是check_all_conditions 是一个坏名字,因为它检查所有条件是否为真。我会使用 matches_any_condition 之类的东西。
      • 这是一个有趣的技巧。我尽量减少我稍后会打错字的字母数量:) 当我真的想提供一个有用的提示时,我似乎在我的解决方案中加入了一堆意见。应该删掉吗?
      • 这似乎太老套了,尤其是考虑到这个问题的其他解决方案。 OP 试图做的事情一点也不复杂。解决方案应该足够简单,可以理解半睡半醒。而且我不知道这里发生了什么。
      • 我的目标是灵活性。修改后的答案以包含一个不那么“hacky”的变体
      【解决方案13】:

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

      基本思想是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”?
      • @NathanBasanese 当然,异常总是用于控制流。 StopIteration 是一个很好的例子:每次你耗尽一个可迭代对象时都会引发一个异常。您要避免的事情是一遍又一遍地连续引发异常,这会变得昂贵。但是一次就不行了。
      • // 啊,我认为你指的是programmers.stackexchange.com/questions/112463/… 之类的东西。我为这个问题和这个答案投票。 Python 3 文档在这里:docs.python.org/3/library/stdtypes.html#iterator-types,我想。
      • 您想定义一个通用函数和一个异常,只是为了在某个其他函数中做一些检查?我觉得这有点多。
      • @BacklightShining 我同意。我自己从来不会真的这样做。 OP要求避免重复代码的方法,但我认为他的开头很好。
      【解决方案14】:

      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
      

      【讨论】:

        【解决方案15】:

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

        我觉得有趣的是,没有人说过函数将返回许多不同的数据类型这一事实,这将强制检查 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 .通过使用funcall_conditions 作为默认值,您可以使用assessed_x(x),或者您可以通过func 传递进一步的个性化生成器。

        这样一来,您会在一张支票通过后立即获得x,但它始终是同一类型。

        【讨论】:

          【解决方案16】:

          如果您需要 Python 3.8,您可以使用“assignment expressions”的新功能来减少 if-else 链的重复性:

          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
          

          【讨论】:

          • 这不是有效的 Python,不。 Python 不允许你像那样使用赋值运算符。但是,最近添加了一个新的特殊赋值表达式,因此您现在可以编写 if ( x := check_size() ) : 以获得相同的效果。
          【解决方案17】:

          我喜欢@timgeb 的。同时我想补充一点,不需要在return 语句中表达None,因为or 分隔语句的集合被评估并且第一个非零、非空、none-None 被返回并且如果没有,则返回None,无论是否有None

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

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

          使用timeitnumber=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
          

          【讨论】:

            【解决方案18】:

            或者使用max:

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

            【讨论】:

              猜你喜欢
              • 2021-06-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2012-06-03
              • 2017-02-05
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多