我怎样才能 "squash" 字典列表?

How can I "squash" a list of dictionaries?

抱歉标题不明确,但我不确定如何描述我正在尝试执行的操作。

django-auditlog 在格式为 {'field_name': [old_value, new_value]} 的 Django 模型中生成 "diffs" 个跟踪字段,这些字段在数据库中更改时跟踪字段。因此,我数据库中特定行上的这些差异列表,首先按照最近的差异排序,可能如下所示:

# 1
[
  {
    'price': [490, 530]
  },
  {
    'status': [7, 1],
  },
  {
    'status': [1, 7],
  },
  {
    'status': [10, 1],
    'price': [0, 490],
    'location': [None, 'Calgary']
  }
]

我想 "squash" 像在 Git 中那样处理此历史记录:获取字段的第一个值和字段的最新值,并删除所有中间值。所以在上面的例子中,我想要以下输出:

# 2
{
  'price': [0, 530],
  'status': [10, 1],
  'location': [None, 'Calgary']
}

请注意,多个 'status''price' 更改已压缩为单个 old/new 对。

我相信我可以通过首先创建一个中间字典来完成此操作,其中 所有 更改被串联起来:

# 3
{
  'price': [[0, 490], [490, 530]],
  'status': [[10, 1], [1, 7], [7, 1]],
  'location': [[None, 'Calgary']]
}

然后提取每个字典元素的第一个列表元素的第一个列表元素,以及每个字典元素的最后一个列表元素的最后一个列表元素。

什么是让 #1 看起来像 #3 的干净且 Pythonic 的方法?

在显示的示例数据中,更改按时间倒序列出。只需遍历构建一组合并字段的列表:每个重复的字段都会更新 'old' 值,'new' 来自第一个更改。

changes = [ 
          {
            'price': [490, 530]
          },
          {
            'status': [7, 1],
          },
          {
            'status': [1, 7],
          },
          {
            'status': [10, 1],
            'price': [0, 490],
            'location': [None, 'Calgary']
          }
    ]

squashed = {}

for delta in changes:
    for field, values in delta.items():
        if field in squashed:
            squashed[field][0] = values[0]
        else:
            squashed[field] = values

产生以下结果:

In [7]: print(squashed)
{'status': [10, 1], 'location': [None, 'Calgary'], 'price': [0, 530]}

您可以直接转到#2。在遍历 #1 时,如果密钥是新的,则创建一个新条目,并且只更新结束状态:

l = [
  {
    'price': [490, 530]
  },
  {
    'status': [7, 1],
  },
  {
    'status': [1, 7],
  },
  {
    'status': [10, 1],
    'price': [0, 490],
    'location': [None, 'Calgary']
  }
]

l.reverse()
squashed = {}
for x in l:
    for k,v in x.items():
        squashed.setdefault(k, [v[0],v[1]])
        squashed[k][1] = v[1]

print squashed

dict.setdefault() 可能对您有用:

from pprint import pprint
one = [
  {
    'price': [490, 530]
  },
  {
    'status': [7, 1],
  },
  {
    'status': [1, 7],
  },
  {
    'status': [10, 1],
    'price': [0, 490],
    'location': [None, 'Calgary']
  }
]

two = {}
for d in one:
    for k,v in d.items():
        two.setdefault(k, v)[0] = v[0]

pprint(two)

结果:

{'location': [None, 'Calgary'], 'price': [0, 530], 'status': [10, 1]}

考虑更新列表:

crunch=lambda d,u: dict(d.items()+[(k, [u[k][0], d.get(k, u[k])[1]]) for k in u])
reduce(crunch, l)

这给你:

{'location': [None, 'Calgary'], 'price': [0, 530], 'status': [10, 1]}

因此,reduce 函数的第一个参数是一个函数,它接收按以下方式从列表中获取的一对参数:

l = [ 0, 1, 2, 3 ]
reduce( f, l ) == f( f ( f( f(0, 1), 2), 3)

通过这种方式,lambda 函数接收一个增量构建的字典作为第一个参数 (d),并通过迭代 u 中的更新来构建一个新的更新字典。

lambda 函数变得过于复杂,因为更新方法不是 return 字典而是 None,所以它正在构建一个新字典,而只是为了能够 return它。

您可以将 lambda 替换为实际函数,作为更清晰的替代方案,可以轻松 return 更新字典:

def crunch(dic, updates):
    dic.update(
        { k: [updates[k][0], dic.get(k, updates[k])[1]] for k in updates }
    )
    return dic  # gonna be the input of the next iteration

然后做:

reduce(crunch, l)

字典的 get 方法 return如果 k 存在则为项目值,如果不存在则将第二个参数作为默认值,因此它不需要 defaultdict 或 setdefault。