如何在列表推导式中设置局部变量?

80

我有一个方法,它接收一个列表并返回一个对象:

# input a list, returns an object
def map_to_obj(lst):
    a_list = f(lst)
    return a_list[0] if a_list else None
我希望获得一个包含所有映射元素但不为None的列表。
像这样:
v_list = [v1, v2, v3, v4]

[map_to_obj(v) for v in v_list if map_to_obj(v)]

但在列表推导中两次调用map_to_obj方法似乎不太好。

是否有办法在列表推导中使用局部变量以便获得更好的性能?

或者编译器会自动进行优化吗?

以下是我的期望:

(sml like)
[let mapped = map_to_obj(v) in for v in v_list if mapped end] 
8个回答

91

Python 3.8 开始,并且引入了 赋值表达式 (PEP 572) (:= 运算符),现在可以在列表推导式中使用本地变量来避免多次调用同一函数。

在我们的情况下,我们可以将 map_to_obj(v) 的评估命名为变量 o,同时使用表达式结果来筛选列表;因此可以使用 o 作为映射值:

[o for v in [v1, v2, v3, v4] if (o := map_to_obj(v))]

2
这样做的好处是不需要两次遍历列表。 - Alex Reinking
@Alex 你的意思是与Lying Dog的答案相比吗?因为与其他答案如Vincent的Bruno的相比,它们不会遍历两次列表。 - wjandrea
1
@wjandrea - 是的,我是指与(仍然)得票最高的答案相比。我在一月份写下了我的评论,在趋势排序正确地将这个答案提升到第一位之前。 - Alex Reinking
1
这真是太棒了。谢谢你提供这个信息。 - Kada B
1
注意这个!如果由于某种原因,map_to_obj(v)将值0赋给o,那么该项将从列表中省略。相反,我建议使用[o for v in [v1, v2, v3, v4] if (o := map_to_obj(v)) is not False]。 就是这样,零赋值有效。天啊,我花了太长时间才弄明白... - undefined

78

使用嵌套列表推导:

[x for x in [map_to_obj(v) for v in v_list] if x]

或者更好的方式是,使用生成器表达式创建一个列表推导式:

[x for x in (map_to_obj(v) for v in v_list) if x]

这是一个很好的答案,当然与behzad的答案相同,只不过使用了列表推导式代替mapfilter...我会点赞,因为我喜欢Lying Dog如何将filter翻译成l-c,但OP可以批准其中任何一个答案,因为两个都是很好的、有用的答案。 - gboffi
4
内部理解应该是一个生成器表达式。无需构建整个列表,然后扔掉空项以构建另一个列表。 - Tim Pietzcker
1
@Lying 在我看来,Tim的建议很中肯和有意义,请考虑编辑您的回答以反映他的建议。 - gboffi
@gboffi 我点赞了这个,而不是Behzad的,因为列表推导通常比map/filter更快,这是由于lambda的调用开销。 - cowbert
1
@cowbert:如果涉及到lambda(实际上是任何Python实现的函数,可以内联到推导式中以避免调用开销),那么列表推导式胜出。但是,如果回调函数是C内置的,则map/filter可能会胜出,因为它们可以将所有工作推送到C层,绕过字节码解释器开销,而列表推导式无法避免,因为它们是Python级别的函数,只是具有直接的字节码支持来执行向列表添加元素的步骤。 - ShadowRanger
1
愚蠢的例子:将behzad的答案更改为使用None.__ne__替换lambda t: t is not None,虽然它的工作方式略微奇怪(它对其他None返回False,对其他所有内容返回恰好为真值的NotImplemented),但可以提高该解决方案的速度(在本地测试中,filter的工作减少了近2倍),尽管这是以理解难度为代价的。 - ShadowRanger

10
变量赋值只是一个单一的绑定:
[x   for v in l   for x in [v]]

这是一个更一般的答案,也更接近于你提出的问题。因此,对于你的问题,你可以这样写:

[x   for v in v_list   for x in [map_to_obj(v)]   if x]

1
这与我的答案仅在使用1个元素列表而不是1个元素元组方面有所不同,我认为当它不需要可变时,元组胜过列表。 :-) - Ovaflo
@Ovaflo 如果我没记错的话,在CPython中有一个特殊情况,使得这里的列表字面量编译为元组。它是固定长度的,不能被改变,所以这是有道理的。但是如果你做类似 [map_to_obj(v)] * n 这样的事情,那当然会创建一个列表。 - wjandrea

7
你可以使用Python内置的filter来避免重新计算:
list(filter(lambda t: t is not None, map(map_to_obj, v_list)))

有没有一次迭代的解决方案? - Hao Tan
2
在Python 3中,@HaoTan不再可用;map返回一个映射对象而不是列表,filter返回一个过滤器对象。因此,这将在不生成中间列表的情况下链接函数。 - behzad.nouri
使用 reduce 能完成这项工作吗? - Hao Tan
1
@HaoTan:你为什么要这样做?reduce是用于将多个元素合并为一个元素,而不是将多个元素转换为多个元素。通过配对的mapfilter(然后进行list化)所完成的工作相当于执行同时进行过滤和映射操作的列表推导式。 - ShadowRanger
值得注意的是,这基本上相当于Lying Dog的答案,只是使用了不同的风格。在Python 3中,它类似于生成器表达式解决方案,在Python 2中则类似于嵌套列表推导式。 - wjandrea

4

可以通过“作弊”的方法,在推导式内设置局部变量,使用额外的“for”循环并迭代包含所需局部变量值的1元组即可。以下是使用此方法解决OP问题的方案:

[o for v in v_list for o in (map_to_obj(v),) if o]

在这里,对于每个v,o是被设置为map_to_obj(v)的局部变量。
在我的测试中,这比Lying Dog的嵌套生成器表达式稍微快一些(也比OP的双重调用map_to_obj(v)更快,令人惊讶的是,如果map_to_obj函数不太慢,它可以比嵌套的生成器表达式更快)。

3

列表推导式(List comprehensions)适用于简单情况,但是有时候普通的 for 循环是最简单的解决方案:

other_list = []
for v in v_list:
    obj = map_to_obj(v)
    if obj:
        other_list.append(obj)

如果你真的想使用列表推导式,而不想构建一个临时列表,可以使用filtermap的迭代器版本:

import itertools as it
result = list(it.ifilter(None, it.imap(map_to_obj, v_list)))

更简单地说:
import itertools as it
result = filter(None, it.imap(map_to_obj, v_list)))

迭代器版本不会建立临时列表,它们使用惰性求值。


0
我已经想出了一种使用 reduce 的方法:
def map_and_append(lst, v):
    mapped = map_to_obj(v)
    if mapped is not None:
        lst.append(mapped)
    return lst

reduce(map_and_append, v_list, [])

这个的性能如何?


4
你可以使用 timeit 模块来计时不同的解决方案,但是你上面的代码片段是一种过度复杂化处理简单问题的任意方式 - 而且我怀疑它既不会比普通的 for 循环或者 filter/imap 解决方案更快,也不会更省空间... - bruno desthuilliers
3
@Bruno 我喜欢你的“过于复杂化”! - gboffi
如果我查看没有使用itertools模块的答案,性能不考虑的话,我敢说Lying Dog的答案最能表达出意图,因此最可读。至于性能呢?嗯,我不知道你在f(lst)调用中花费了多少CPU,但是无论如何删除None都不太可能改变整个情况。 - gboffi
1
这里并不需要使用 reduce。它的作用是将一个值列表减少为单个值(通常是将标量列表减少为单个标量),而不是将一个列表转换为另一个列表。 - chepner
注意,这会修改lst的副作用,我认为这是一个错误,尽管只有在您持有另一个引用时才会出现。您可以通过避免使用.append()而改为使用return lst+[mapped] if mapped is not None else lst来修复它。 - wjandrea

0
我发现做到这一点的最好方法,虽然神秘但简洁明了,就是这样:
[f(y) for x in some_list()
      for y in [some_item(x)]  # wastefully making a singleton list
      if some_condition(y)]

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接