ChatGPT解决这个技术问题 Extra ChatGPT

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

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

C
CompuChip

除了 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 (....) 根据需要插入换行符。不过,这将是一条很长的逻辑线。
我认为这是更好的解决方案。 “如果有多个选项,它将变得乏味 [..]”的论点是没有实际意义的,因为无论如何单个函数都不应该进行过多的检查。如果需要,则应将检查拆分为多个功能。
M
Martijn Pieters

你可以使用一个循环:

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 秒才能被我的 Brainparser 解析。
“更喜欢 Python”,他们说。 “Perl 是不可读的”,他们说。然后发生了这种情况:return next((check for check in checks if check), None)
J
Jack Aidley

不要改变它

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


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

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

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

请大家帮忙将这个答案提高到第一名。尽你的一份力量!
P
Phil Frost

根据 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
佚名

这是 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;之后我的眼睛变得呆滞
z
zwol

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


A
Arnaud Le Blanc

我很惊讶没有人提到为此目的而制作的内置 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 或 f() 是检查累加器 a 或当前检查 f() 是否为 True 的函数。请注意,如果 a 为 True,则不会计算 f()。

checks 包含检查函数(来自 lambda 的 f 项)

False 是初始值,否则不会发生检查,结果将始终为 True

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


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

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

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

如果你看一下,我认为这看起来很好而且很清楚。

演示:

https://i.stack.imgur.com/gYcy8.png


终端上方的小 ASCII 鱼是怎么回事?
@LegoStormtroopr 我用的是鱼壳,所以我用 ascii 鱼缸装饰它让我开心。 :)
谢谢你的好鱼(顺便说一下颜色,那是哪个编辑器?)
您可以在 fishshell.com 获取 fish,在 pastebin.com/yYVYvVeK 获取 ascii 的配置文件,编辑器也是 sublime 文本。
x if x else <something> 可以简化为 x or <something>
j
juandesant

对我来说,最好的答案是来自@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。通过使用 funcall_conditions 作为默认值,您可以使用 assessed_x(x),或者您可以通过 func 传递进一步的个性化生成器。

这样,只要一张支票通过,您就会得到 x,但它始终是相同的类型。


R
RoadieRich

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

if check_size(x):
    return x
#etc

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


R
Reza Dodge

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

m
mathreadler

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

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

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

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

基本思想是 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 要求避免重复代码的方法,但我认为他开始的方法非常好。
D
Dmitry Rubanovich

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

z
zwol

如果您需要 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() ) : 以获得相同的效果。
U
U12-Forward

或使用 max

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

C
Carel

过去,我已经看到了一些有趣的带有 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。这对于长时间运行的函数很有用。 dict 中的 lambdas 将其值的执行延迟到 while 循环。

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

timeit 的同事可能会为向堆栈中添加一两个额外层的成本以及 dict 查找的成本讨价还价,但您似乎更关心代码的美观性。

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

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 可能遵循一些编码约定,使用一些现有的代码库或不关心其代码库中的简洁术语。


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