【问题标题】:Python Generator Expression for Accumulating Dictionary Values用于累积字典值的 Python 生成器表达式
【发布时间】:2012-03-06 08:36:22
【问题描述】:

生成器表达式正在抛出大量元组对,例如。列表形式:

pairs = [(3, 47), (6, 47), (9, 47), (6, 27), (11, 27), (23, 27), (41, 27), (4, 67), (9, 67), (11, 67), (33, 67)]

对于成对的每一对,key = pair[0] 和 value = pair[1],我想将这个对流输入到字典中,以累积添加各个键的值。显而易见的解决方案是:

dict_k_v = {}
for pair in pairs:
    try:
        dict_k_v[pair[0]] += pair[1]
    except:
        dict_k_v[pair[0]] = pair[1]

>>> dict_k_v
{33: 67, 3: 47, 4: 67, 6: 74, 9: 114, 11: 94, 41: 27, 23: 27}

但是,这可以通过生成器表达式或不使用 for 循环的类似构造来实现吗?

编辑

为了澄清,生成器表达式抛出了大量的元组对:

(3, 47), (6, 47), (9, 47), (6, 27), (11, 27), (23, 27), (41, 27), (4, 67) , (9, 67), (11, 67), (33, 67) ...

并且我想在生成每对键值对时将每个键值对累积到字典中(请参阅 Paul McGuire 的答案)。 pair = list[] 声明是不必要的,对此感到抱歉。对于每一对 (x,y),x 是整数,y 可以是整数或小数/浮点数。

我的生成器表达式是这样的:

((x,y) for y in something() for x in somethingelse())

并希望将每个 (x,y) 对累积到默认字典中。嗯。

【问题讨论】:

  • 最近对 for 循环的厌恶是什么?将累加包裹到 defaultdict 中的 for 循环是最干净的解决方案。
  • 我刚刚就推入 dict 的所有选项进行了长时间讨论,结果证明最有效的编码方法是使用 if key in dict: / else: (不是你想要的使用 for 循环 :-)
  • @PaulMcGuire 当数据集非常大和/或连续执行操作时,对 for 循环的主要厌恶可能会影响性能。一种选择是 Cython,但我想看看是否有使用内置函数的 Python 解决方案。
  • @pietdelport 在他的回答中,Paul McGuire 明确地添加了我认为显而易见的问题(哎呀!),即。 “...接受发送给它的每个键值对,并将它们全部累积到传递给它的 defaultdict 中”。我已将此添加到原始问题中。

标签: python dictionary generator


【解决方案1】:

您可以使用元组解构和 defaultdict 来大大缩短该循环:

from collections import defaultdict
d = defaultdict(int)
for k,v in pairs: d[k] += v

这仍然使用 for 循环,但您不必处理以前未见过键的情况。我认为这可能是最好的解决方案,无论是可读性还是性能方面。

使用groupby 的概念证明

也就是说,您可以使用itertools.groupby 来实现,但这有点小技巧:

import itertools
dict((k, sum(v for k,v in group)) for k, group 
     in itertools.groupby(sorted(pairs), lambda (k,v): k))

此外,这实际上应该比第一种方法性能更低,因为需要为排序创建所有对的内存列表。

【讨论】:

  • 鉴于 OP 声明他们正在使用“生成器表达式”和“大量”对,我会支持 defaultdict 解决方案而不是任何 sort+groupby 解决方案,因为for 循环干净地处理对流,将总数汇总到 defaultdict 的条目中,并且不需要创建中间的内存中值列表(就像在内部通过 sorted 完成的那样)。
  • @Paul:这正是我的观点,但回顾那个答案,我认为使用for-loop 的解决方案并不是绝对明显的。
【解决方案2】:

不,如果不使用 some 形式的循环,您将无法做到这一点。并且使用for 循环确实是最明智的做法,因为您正在修改循环主体中的某些内容(而不是,例如,创建新的迭代或列表。)但是,您可以通过使用来简化代码collections.defaultdict,像这样:

import collections
dict_k_v = collections.defaultdict(int)
for k, v in pairs:
    dict_k_v[k] += v

【讨论】:

  • 您可以使用不是循环结构的递归来做到这一点,但我想可能是“某种”形式的循环。这并不是说您应该使用递归来解决这个特殊问题。
【解决方案3】:

类似:

dict_k_v = dict(pairs)

【讨论】:

  • 这不会对重复的键做正确的事情。 (请不要删除答案,否则我们只会得到更多人的建议。)
【解决方案4】:
>>> dict((x[0], sum(y[1] for y in x[1])) for x in itertools.groupby(sorted(pairs, key=operator.itemgetter(0)), key=operator.itemgetter(0)))
{33: 67, 3: 47, 4: 67, 6: 74, 9: 114, 11: 94, 41: 27, 23: 27}

【讨论】:

  • 不应该sorted默认按字典顺序比较?
  • @Niklas:当然。但我不关心第二个元素,所以我不理会它,以免它妨碍sorted
  • @IgnacioVazquez-Abrams 您的解决方案短小精悍,性能卓越,适合我的其余代码。因为我要求一个字典解决方案,在生成对时累积它们,所以接受你的解决方案是不公平的。对不起!
【解决方案5】:

您可以实现递归调用,但 Python 并未针对尾递归进行优化,因此您将付出速度代价并有可能出现“深度递归”异常。

import operator as o
def dict_sum(pairs, totals={}):
  k, v = pairs.pop()
  o.setitem(sum, k, totals.get(k, 0) + v)
  if not pairs:
    return totals
  else:
    return dict_sum(pairs, totals)

我会在 for 循环中实现它:

import operator as o
totals={}
for k, v in pairs:
   o.setitem(totals, k, totals.get(k, 0) + v)

【讨论】:

    【解决方案6】:

    为什么不想使用 for 循环?

    pairs = [(3, 47), (6, 47), (9, 47), (6, 27), (11, 27), (23, 27), (41, 27), (4, 67), (9, 67), (11, 67), (33, 67)]
    result={}
    def add(pair):
        k,v=pair
        result[k]=result.get(k,0)+v
    map(add,pairs)
    print result
    

    【讨论】:

      【解决方案7】:

      为了讨论,这里有一个简单的生成器函数来给我们一些数据:

      from random import randint
      def generator1():
          for i in range(10000):
              yield (randint(1,10), randint(1,100))
      

      这是一个基本的解决方案,它使用 Python for 循环来消耗生成器并计算每个键值对的计数

      from collections import defaultdict
      
      tally = defaultdict(int)
      for k,v in generator1():
          tally[k] += v
      
      for k in sorted(tally):
          print k, tally[k]
      

      将打印如下内容:

      1 49030
      2 51963
      3 51396
      4 49292
      5 51908
      6 49481
      7 49645
      8 49149
      9 48523
      10 50722
      

      但是我们可以创建一个协程来接受发送给它的每个键值对,并将它们全部累积到传递给它的 defaultdict 中:

      # define coroutine to update defaultdict for every
      # key,value pair sent to it
      def tallyAccumulator(t):
          try:
              while True:
                  k,v = (yield)
                  t[k] += v
          except GeneratorExit:
              pass
      

      我们将用一个默认的默认字典初始化协程,并通过向它发送一个 None 值来准备接受值:

      # init coroutine
      tally = defaultdict(int)
      c = tallyAccumulator(tally)
      c.send(None)
      

      我们可以使用 for 循环或列表推导将所有生成器值发送到协程:

      for val in generator1():
          c.send(val)
      

      [c.send(val) for val in generator1()]
      

      但是,我们将使用一个大小为零的双端队列来处理所有生成器表达式的值,而不会创建不必要的 None 临时列表:

      # create generator expression consumer
      from collections import deque
      do_all = deque(maxlen=0).extend
      
      # loop thru generator at C speed, instead of Python for-loop speed
      do_all(c.send(val) for val in generator1())
      

      现在我们再看一下这些值:

      for k in sorted(tally):
          print k, tally[k]
      

      我们得到另一个与第一个类似的列表:

      1 52236
      2 49139
      3 51848
      4 51194
      5 51275
      6 50012
      7 51875
      8 46013
      9 50955
      10 52192
      

      在 David Beazley 的页面上阅读有关协程的更多信息:http://www.dabeaz.com/coroutines/

      【讨论】:

      • 这个答案写得很好,但动机很差:与使用普通的生成器消费者相比,使用协程样式的生成器不会为解决方案增加任何价值。
      • @PietDelport - 至少感谢您不反对我。我同意这对于这个特定问题来说是极端的矫枉过正,但它似乎是展示协程演示的一个很好的练习。
      • 让我的生成器可以使用:do_all(c.send((x,y)) for y in something() for x in somethingelse()) 带有包含累积值的 defaultdict 计数相应的键。
      • 您的嵌套循环将返回所有成对的 x 和 y 值。如果这确实是您想要的,您可以重写为for x,y in itertools.product(something(), somethingelse()) 如果您只想配对两个函数返回的值,请使用 zip 而不是 product。另请注意,somethingelse() 被调用了 y 次 - 不确定产品是否足够聪明,可以避免这种情况。
      • @PaulMcGuire 谢谢,保罗。有足够的可以继续前进。巧合的是,最近正在看 Beazley 的协程工作,现在有了一个完美的例子。稍后将在 Ignacio 和您的解决方案之间执行计时。
      【解决方案8】:

      Haskell 有一个非常好的通用助手:Data.Map's fromListWith

      fromListWith 类似于 Python 的 dict 构造函数,但它也接受一个额外的组合函数来组合重复键的值。将其翻译成 Python:

      def dict_fromitems(items, combine):
          d = dict()
          for (k, v) in items:
              if k in d:
                  d[k] = combine(d[k], v)
              else:
                  d[k] = v
          return d
      

      使用这个助手,很容易表达多种组合:

      >>> import operator
      >>> dict_fromitems(pairs, combine=operator.add)
      {33: 67, 3: 47, 4: 67, 6: 74, 9: 114, 11: 94, 41: 27, 23: 27}
      
      >>> dict_fromitems(pairs, combine=min)
      {33: 67, 3: 47, 4: 67, 6: 27, 9: 47, 11: 27, 41: 27, 23: 27}
      
      >>> dict_fromitems(pairs, combine=max)
      {33: 67, 3: 47, 4: 67, 6: 47, 9: 67, 11: 67, 41: 27, 23: 27}
      
      >>> dict_fromitems(((k, [v]) for (k, v) in pairs), combine=operator.add)
      {33: [67], 3: [47], 4: [67], 6: [47, 27], 9: [47, 67], 11: [27, 67], 41: [27], 2
      3: [27]}
      

      请注意,与使用defaultdict(int) 的解决方案不同,此方法不限于数值,如上面的列表示例所示。 (一般来说,任何幺半群都是一种有用的可能性:带有联合/交集的集合、带有和/或的布尔值、带有连接的字符串等等。)

      附录

      正如其他 cmets 指出的那样,为此使用循环并没有错:它是适当的低级解决方案。但是,如果您可以将低级代码包装在可重用的高级抽象中,那总是好的。

      【讨论】:

        猜你喜欢
        • 2013-02-25
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-06-28
        • 2017-12-08
        • 2014-04-23
        • 2018-04-15
        • 2021-05-13
        相关资源
        最近更新 更多