【问题标题】:Python list comprehension - want to avoid repeated evaluationPython列表理解 - 想要避免重复评估
【发布时间】:2013-04-04 13:32:08
【问题描述】:

我的列表理解近似于:

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

其中 l 是一个列表,f(x) 是一个返回列表的昂贵函数。

我想避免对 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)]

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

【问题讨论】:

  • 好吧,即使在这种情况下,您也确实可以通过生成器理解来解决它,只需 [g(x, fx) for x, fx in ((x,f(x)) for x in l) if fx]。重点是x中是否有任何重复。
  • 谢谢,看来一切都可以通过理解来解决!我仍然认为,一旦表达式变得如此复杂,记忆会使代码更具可读性。
  • 是的,使用生成器(带括号,而不是方括号)。如果您更喜欢记忆化,那很好,但是生成器比构建然后过滤整个列表要好得多,就像您现在所做的那样。 (例如,如果内部生成器是无限的,则可以使用它,而外部理解在找到某个值时停止)。

标签: python list-comprehension code-readability


【解决方案1】:
[y for y in (f(x) for x in l) if y]

会的。

【讨论】:

    【解决方案2】:

    一个解决方案(如果你有重复的 x 值最好)是 memoize 函数 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,而如果第二个失败,则可以使用一些更稳健的方法。

    你可以在网上找到很多memoization的实现,我认为python的新版本也包含了一些东西。

    附带说明,永远不要使用小 L 作为变量名,这是一个坏习惯,因为它可能与某些终端上的 i 或 1 混淆。

    编辑:

    正如所评论的,使用生成器理解(以避免创建无用的重复临时对象)的可能解决方案是以下表达式:

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

    考虑到 f 的计算成本、原始列表中的重复次数以及您所使用的内存,您需要权衡您的选择。记忆化会在空间速度上进行权衡,这意味着它会跟踪保存它的每个结果,因此如果您有大量列表,则在内存占用方面可能会变得很昂贵。

    【讨论】:

    • 我的回答差不多,只有你可以使用@memoize 装饰器。
    • 这是 memoization 装饰器的粗略实现,确实用于解释目的。关于性能,只有当每个 x 彼此不同时,其他解决方案才是最差的。如果甚至一个与另一个相同,它只会遭受字典的开销,这是边际的,因为 f 已被定义为代价高昂的函数。这意味着即使重复一次,它也会在函数调用减少方面获得很多。它不像其他解决方案那样重要,但它更强大......
    • 我编辑了我的评论以获得更好的解释。我个人不喜欢 memoization,但在这类问题中,我认为它是更健壮的解决方案,我没有不假思索地使用它。
    • 这真的有用吗?您的函数全局更改storage,这意味着您需要在对一组新数据调用它之前重置storage——这样您就可以轻松地使用列表理解。恕我直言,这不值得。只需使用一个循环。
    • 和以前一样,如果 f 是确定性的,那就更好了,因为之前已经看到的第二次迭代中的任何值都不需要第二次评估。事实上,你需要执行循环的次数越多,记忆就越有用。每种技术都有其好的一面和坏的一面。
    【解决方案3】:

    你应该使用 memoize 装饰器。这是一个有趣的link


    使用链接中的记忆和您的“代码”:

    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)]
    

    【讨论】:

    • 这是我最喜欢的这里提供的记忆选项。您仍然可以通过f.f(...) 访问该函数,它以每个函数为基础(而不是在全局基础上)保持状态,它使用dict.__missing__,这非常有用。你有我的赞成票。
    【解决方案4】:

    Python 3.8 开始,并引入assignment expressions (PEP 572):= 运算符),可以在列表解析中使用局部变量以避免调用两次相同的函数:

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

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

    【讨论】:

      【解决方案5】:
      [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
      • 你甚至可以将内部理解变成一个生成器,以节省重复列表的创建。
      • 如果我提前将内部生成器分解为临时变量,我可能会使用它...
      • 这个看起来像@Mahdi 的解决方案,但它会在过滤之前构建整个列表。 Mahdi 的更好:它会创建一个生成器并一次抽取一个值。
      【解决方案6】:

      不。没有 (clean) 方法可以做到这一点。老式的循环没有错:

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

      如果您觉得难以阅读,您可以随时将其包装在一个函数中。

      【讨论】:

        【解决方案7】:

        正如前面的答案所示,您可以使用双重理解或使用记忆。对于合理大小的问题,这是一个品味问题(我同意记忆化看起来更干净,因为它隐藏了优化)。但是,如果您正在检查一个非常大的列表,会有很大的不同:记忆化会存储您计算出的每一个值,并且会很快耗尽您的记忆。 带有生成器的双重理解(圆括号,而不是方括号)只存储您想要保留的内容。

        来到你的实际问题:

        [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] 的内存占用应该是不同的吗?什么时候清理记忆列表(取决于记忆函数定义在哪里)?
        • 计算出的最终列表是相同的,但您需要更长的时间才能到达那里(垃圾收集也需要时间)。
        • 附言。但不要相信我的话,试试看并计时!
        【解决方案8】:

        关于记忆有很多答案。 Python 3 标准库现在有一个lru_cache,这是一个最近使用的缓存。所以你可以:

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

        这样你的函数只会被调用一次。您还可以指定lru_cache 的大小,默认为 128。上面显示的 memoize 装饰器的问题是列表的大小会变得无法控制。

        【讨论】:

        • 这看起来很有用。如果我正确理解列表解析的工作原理,我可以使用大小为 1 的 lru_cache,因为我只想重用最近使用的值?
        【解决方案9】:

        您可以使用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 装饰器来记忆任何函数。

        【讨论】:

          【解决方案10】:

          使用map()!!

          comp = [x for x in map(f, l) if x]
          

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

          map() 将为列表中的每个 x 返回 f(x) 的结果。

          【讨论】:

            【解决方案11】:

            这是我的解决方案:

            filter(None, [f(x) for x in l])
            

            【讨论】:

            • 和其他人一样,您可以在filter 中使用生成器——我通常更喜欢filter(bool,...) 而不是filter(None,...)。做同样的事情,但我发现前者更明确。例如我认为这会更好filter(bool,(f(x) for x in l))
            【解决方案12】:

            如何定义:

            def truths(L):
                """Return the elements of L that test true"""
                return [x for x in L if x]
            

            这样,例如

            > [wife.children for wife in henry8.wives]
            [[Mary1], [Elizabeth1], [Edward6], [], [], []]
            
            > truths(wife.children for wife in henry8.wives) 
            [[Mary1], [Elizabeth1], [Edward6]]
            

            【讨论】:

              猜你喜欢
              • 2011-04-03
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2015-06-13
              • 1970-01-01
              • 2015-01-02
              • 1970-01-01
              相关资源
              最近更新 更多