关于python:pythonic方法避免“if x:return x”语句

Pythonic way to avoid “if x: return x” statements

我有一个方法,按顺序调用其他4个方法来检查特定的条件,并在每次返回Truthy时立即返回(不检查以下的方法)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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语句,我宁愿做类似的事情:

1
x and return x

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


除了Martijn的好答案,你可以链接or。这将返回第一个truthy值,如果没有真值,则返回None

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

演示:

1
2
3
4
5
6
>>> x = [] or 0 or {} or -1 or None
>>> x
-1
>>> x = [] or 0 or {} or '' or None
>>> x is None
True


你可以使用一个循环:

1
2
3
4
5
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版本)来获得第一个匹配值:

1
2
3
4
5
6
7
8
9
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)

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

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

1
2
3
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)


不要改变它

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


在与timgeb有效的答案中,您可以使用括号进行更好的格式化:

1
2
3
4
5
6
7
8
def check_all_the_things():
    return (
        one()
        or two()
        or five()
        or three()
        or None
    )


根据Curly定律,您可以通过分割两个问题来使这些代码更具可读性:

  • 我要检查什么?
  • 有一件事是真的吗?

分为两个功能:

1
2
3
4
5
6
7
8
9
10
11
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

这避免了:

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

...同时保留线性,易读的流程。

根据您的特定情况,您可能还可以提供更好的功能名称,这使其更具可读性。


这是Martijns第一个例子的变种。它还使用"callables"风格,以便允许短路。

您可以使用builtin any而不是循环。

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

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


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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它读得更顺畅。


我很惊讶没有人提到为此目的而制作的内置any

1
2
3
4
5
6
7
def check_all_conditions():
    return any([
        check_size(),
        check_color(),
        check_tone(),
        check_flavor()
    ])

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

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

1
2
3
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]) : Apply function of two
arguments cumulatively to the items of iterable, from left to right,
so as to reduce the iterable to a single value. The left argument, x,
is the accumulated value and the right argument, y, is the update
value from the iterable. If the optional initializer is present, it is
placed before the items of the iterable in the calculation

在你的情况下:

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

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


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

1
2
3
4
5
6
7
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

如果你看一下,我觉得这很好看。

演示:

Screenshot of it running


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

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

所以我会将@ PhilFrost的回复与保持单一类型的想法混合在一起:

1
2
3
4
5
6
7
8
9
10
11
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,但它总是相同的类型。


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

1
2
3
if check_size(x):
    return x
#etc

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


Martijns上面的第一个例子略有不同,它避免了循环内部的if:

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


这种方式有点偏离框,但我认为最终结果是简单,可读,并且看起来不错。

当其中一个函数计算为真值时,基本思想是raise异常,并返回结果。以下是它的外观:

1
2
3
4
5
6
7
8
9
10
11
def check_conditions():
    try:
        assertFalsey(
            check_size,
            check_color,
            check_tone,
            check_flavor)
    except TruthyException as e:
        return e.trigger
    else:
        return None

你需要一个assertFalsey函数,当其中一个被调用的函数参数计算为truthy时,它会引发一个异常:

1
2
3
4
5
def assertFalsey(*funcs):
    for f in funcs:
        o = f()
        if o:
            raise TruthyException(o)

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

当然,你需要TruthyException本身。此异常提供触发异常的object

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

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

1
2
3
4
5
6
7
8
9
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方式是使用reduce(如已经提到的那样)或itertools(如下所示),但在我看来,简单地使用or运算符的短路产生更清晰的代码

1
2
3
4
5
6
7
8
9
10
11
12
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

我喜欢@ timgeb's。 与此同时,我想补充说,不需要在return语句中表达None,因为会计算or分隔语句的集合,并返回第一个非零,非空,无 - 无,并且 如果没有,则返回None是否有None

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

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

使用timeitnumber=10**7我查看了许多建议的运行时间。 为了便于比较,我只使用random.random()函数根据随机数返回字符串或None。 这是整个代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
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()

以下是结果:

1
2
3
4
5
6
Bernard: 7.398444877040768
Martijn Pieters: 8.506569201346597
timgeb: 7.244275416364456
Reza: 6.982133448743038
Phinet: 7.925932800076634
Phil Frost: 11.924794811353031

或者使用max

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


我要跳到这里并且从未写过一行Python,但我认为if x = check_something(): return x是有效的吗?

如果是这样:

1
2
3
4
5
6
7
8
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


我在过去看过一些有关switchs / case语句的有趣实现,这使我得到了这个答案。使用您提供的示例,您将获得以下内容。 (这是疯狂using_complete_sentences_for_function_names,所以check_all_conditions被重命名为status。见(1))

1
2
3
4
5
6
7
8
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。这对于长时间运行的功能很有用。 dict中的lambdas延迟执行它的值直到while循环。

作为奖励,您可以修改执行顺序,甚至可以通过更改ks来跳过某些测试。 k='c',s={'c':'b','b':None}减少了测试次数并反转了原始处理顺序。

timeit研究员可能会讨厌在堆栈中添加额外一层或两层的成本,并且dict的成本会有所提高,但您似乎更关心代码的可爱性。

或者,更简单的实现可能如下:

1
2
3
4
5
6
7
8
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
  • 我的意思是这不是在pep8方面,而是在使用一个简洁的描述性词语代替句子方面。假设OP可能遵循一些编码约定,处理一些现有的代码库或不关心其代码库中的简洁术语。