Python列表推导式 - 希望避免重复评估

66

我有一个列表推导式,大致如下:

[f(x) for x in l if f(x)]

假设l是一个列表,f(x)是一个昂贵的函数,它返回一个列表。

我希望避免在每个非空出现 f(x) 的情况下对其进行两次评估。有没有办法在列表推导中保存它的输出?

我可以删除最后的条件,生成整个列表然后修剪它,但那似乎很浪费。

编辑:

已经提出了两种基本方法:

一种是内部生成器推导式:

[y for y in (f(x) for x in l) if y]

或记忆化。

我认为内部生成器推导式对于所述问题非常优雅。实际上,我简化了问题以使其更清晰明了,我真正想要的是:

[g(x, f(x)) for x in l if f(x)]

对于这种更为复杂的情况,我认为记忆化可以产生更清晰的结果。


5
你确实可以使用生成器推导式来解决这个问题,即使在这种情况下也是如此,只需使用[g(x, fx) for x, fx in ((x,f(x)) for x in l) if fx]。主要问题是x是否有任何重复。 - EnricoGiampieri
谢谢,看起来使用推导式可以解决所有问题!不过我认为,一旦表达式变得复杂,使用记忆化可以使代码更易读。 - Stefan
2
是的,请使用生成器(带括号,而不是方括号)。如果您喜欢记忆化,那也可以,但是与您现在构建并过滤整个列表相比,生成器要好得多。 (例如,如果内部生成器是无限的,并且外部理解在找到某个值时停止,则可以使用它)。 - alexis
12个回答

52
[y for y in (f(x) for x in l) if y]

好的。


14

Python 3.8开始,并引入了赋值表达式(PEP 572):=运算符),可以在列表推导式中使用局部变量,以避免两次调用相同的函数:

在我们的情况下,我们可以将f(x)的计算命名为变量y,同时使用表达式的结果来过滤列表,也作为映射值:

[y for x in l if (y := f(x))]

11
一个解决方案(如果你有重复的 x 值,那么这是最好的)是对函数 f 进行记忆化处理,即创建一个包装函数来保存调用该函数的参数,并将其保存起来,然后在请求相同值时返回它。
以下是一个非常简单的实现:

如果您有重复的 x 值,则 记忆化 函数 f 是一个最佳解决方案,即创建一个包装函数保存调用该函数的参数,然后如果请求相同的值,则返回已保存的结果。

以下是一个非常简单的实现:

storage = {}
def memoized(value):
    if value not in storage:
        storage[value] = f(value)
    return storage[value]

[memoized(x) for x in l if memoized(x)]

然后在列表推导式中使用这个函数。这种方法在两个条件下是有效的,一个是理论上的,一个是实际上的。第一个条件是函数f必须是确定性的,即给定相同的输入返回相同的结果,而另一个条件是对象x可以用作字典键。如果第一个条件无效,则必须根据定义每次重新计算f,而如果第二个条件失败,则可以使用一些稍微更强大的方法。

您可以在网络上找到许多备忘录化的实现,我认为新版本的Python也包括了一些内容。

顺便提一句,永远不要使用小写字母L作为变量名,因为它可能会在某些终端上与i或1混淆,这是一种不好的习惯。

编辑:

正如评论所述,使用生成器推导式(以避免创建无用的重复临时变量)的可能解决方案是此表达式:

[g(x, fx) for x, fx in ((x,f(x)) for x in l) if fx]

考虑到函数f的计算成本、原始列表中重复元素的数量以及您拥有的内存,您需要权衡选择。记忆化(Memoization)进行了一种空间-速度权衡,这意味着它会跟踪每个结果并将其保存下来,因此如果您有巨大的列表,则可能会在内存占用方面变得昂贵。


2
基本上这就是我的答案,只不过你可以使用一个@memoize装饰器。 - Inbar Rose
这只是一个粗略的实现,用来解释记忆化装饰器的目的。就性能而言,它只有在每个x都不同的情况下才比其他解决方案差。如果有一个x与另一个x相同,它只会受到字典开销的影响,但鉴于f已被定义为一个昂贵的函数,这种开销微不足道。这意味着即使有一个重复,它在减少函数调用方面也会获得很大的好处。它并非像其他解决方案那样重要,但更加健壮。 - EnricoGiampieri
我修改了我的评论以便更好地解释。我个人不太喜欢记忆化,但在这种问题中,我想它是更强大的解决方案,并且我没有像银弹一样轻率使用它。 - EnricoGiampieri
5
这真的有用吗?你的函数会全局地改变“storage”,这意味着在对新数据调用此函数之前,需要重置“storage”-- 这样做只是为了能够轻松使用列表推导式。在我看来,这不值得。只需使用循环即可。 - mgilson
如果函数f是确定性的,那会更好,因为第二次迭代中已经出现过的任何值都不需要进行第二次评估。实际上,您需要执行循环的次数越多,记忆化就越有用。每种技术都有其优点和缺点。 - EnricoGiampieri

10

你应该使用一个记忆化装饰器。这里有一个有趣的链接


使用链接中的记忆化以及你的'代码':

def memoize(f):
    """ Memoization decorator for functions taking one or more arguments. """
    class memodict(dict):
        def __init__(self, f):
            self.f = f
        def __call__(self, *args):
            return self[args]
        def __missing__(self, key):
            ret = self[key] = self.f(*key)
            return ret
    return memodict(f)

@memoize
def f(x):
    # your code

[f(x) for x in l if f(x)]

1
这是我在这里提供的记忆化选项中最喜欢的一个。您仍然可以通过 f.f(...) 访问该函数,它基于每个函数保持状态(而不是全局基础),它使用了 dict.__missing__,这非常有用。您有我的支持。 - mgilson

9
[y for y in [f(x) for x in l] if y]

针对您更新后的问题,以下内容可能会有所帮助:

[g(x,y) for x in l for y in [f(x)] if y]

好吧...你赢了,确实有一种方法可以做到这一点。不过我仍然会使用循环 :-P - mgilson
你甚至可以将内部的推导式变成一个生成器,以节省重复列表创建的开销。 - 9000
1
如果我事先将内部生成器分解为临时变量,我可能会使用这个... - mgilson
1
这看起来像是 @Mahdi 的解决方案,但它会在过滤之前构建整个列表。Mahdi的更好:它将创建一个生成器并一次泵送一个值。 - alexis

8

没有(简洁的)方法可以做到这一点。使用传统的循环方法并没有什么问题:

output = []
for x in l:
    result = f(x)
    if result: 
        output.append(result)

如果你觉得这段内容难以理解,你可以将其封装在一个函数中。

8
如前面的答案所示,您可以使用双重推理或使用记忆化。对于规模合理的问题而言,这是一种口味问题(我同意记忆化看起来更干净,因为它隐藏了优化)。但是,如果您要检查非常大的列表,就会有很大的差异:记忆化将存储您计算过的每个单个值,并且可能会快速消耗您的内存。带有生成器的双重推理(圆括号,而不是方括号)仅存储您想要保留的内容。
针对您实际的问题:
[g(x, f(x)) for x in series if f(x)]

要计算最终值,您需要同时拥有 xf(x)。没问题,可以像这样同时传递它们:

[g(x, y) for (x, y) in ( (x, f(x)) for x in series ) if y ]

再次提醒:应该使用生成器(圆括号)而不是列表推导式(方括号)。否则,在开始过滤结果之前,您将构建整个列表。以下为列表推导式版本:

[g(x, y) for (x, y) in [ (x, f(x)) for x in series ] if y ] # DO NOT USE THIS

除了可读性之外,如果记忆化函数定义的位置不同,[g(x, y) for x in series for y in [f(x)] if y] 在内存占用方面会有所不同吗?列表何时被清理取决于记忆化函数的定义位置。 - user110954
计算出的最终列表是一样的,但你需要更长时间才能到达那里(并且垃圾回收也需要时间)。 - alexis
1
但不要只听我的话,试一试并计时! - alexis

4

关于记忆化的问题,已经有很多解答了。Python 3标准库现在有一个lru_cache,它是最近最少使用缓存。因此,您可以这样做:

from functools import lru_cache

@lru_cache()
def f(x):
    # function body here

这样一来,你的函数只会被调用一次。此外,你还可以指定 lru_cache 的大小,默认值为 128。上述 memoize 装饰器的问题在于列表的大小可能会失控。


这看起来很有用。如果我正确理解列表推导式的工作原理,我可以使用 lru_cache 大小为1,因为我只想重复使用最近使用过的值? - Stefan

3
使用 map() !!
comp = [x for x in map(f, l) if x]

f 是函数 f(X)l 是列表

map() 会对列表中的每个 x 返回函数 f(x) 的结果。


3
你可以使用记忆化技术(Memoization)。这是一种技术,通过保存每个计算值的结果来避免重复计算。我看到已经有一个使用memoization的答案了,但我想提出一个通用的实现,使用Python装饰器:
def memoize(func):
    def wrapper(*args):
        if args in wrapper.d:
            return wrapper.d[args]
        ret_val = func(*args)
        wrapper.d[args] = ret_val
        return ret_val
    wrapper.d = {}
    return wrapper

@memoize
def f(x):
...

现在f是其自身的记忆化版本。 使用这种实现方式,您可以使用@memoize装饰器来记忆化任何函数。


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