【问题标题】:Recursive diff of two dictionaries (keys and values)?两个字典(键和值)的递归差异?
【发布时间】:2011-08-19 17:03:00
【问题描述】:

所以我有一个 python 字典,叫它d1,以及以后那个字典的一个版本,叫它d2。我想找出d1d2 之间的所有变化。换句话说,添加、删除或更改的所有内容。棘手的一点是值可以是整数、字符串、列表或字典,因此它需要是递归的。这是我目前所拥有的:

def dd(d1, d2, ctx=""):
    print "Changes in " + ctx
    for k in d1:
        if k not in d2:
            print k + " removed from d2"
    for k in d2:
        if k not in d1:
            print k + " added in d2"
            continue
        if d2[k] != d1[k]:
            if type(d2[k]) not in (dict, list):
                print k + " changed in d2 to " + str(d2[k])
            else:
                if type(d1[k]) != type(d2[k]):
                    print k + " changed to " + str(d2[k])
                    continue
                else:
                    if type(d2[k]) == dict:
                        dd(d1[k], d2[k], k)
                        continue
    print "Done with changes in " + ctx
    return

除非值是一个列表,否则它工作得很好。如果没有在if(type(d2) == list) 之后重复这个函数的巨大的、稍微改变的版本,我无法想出一个优雅的方式来处理列表。

有什么想法吗?

编辑:这与 this post 不同,因为键可以更改

【问题讨论】:

  • 示例:list1 = [0, 1, 2, 3, 4, 5, 6, 7]list2 = [0, 2, 3, 4, 5, 6, 7, 8]。你期望什么输出?
  • 如果它们在 2 个不同的字典中使用相同的键,我认为:1 被删除;添加了 8 个(在同一键下)。如果它们在不同的键下,那么它们就是不同的元素。
  • 这很快就会变得棘手。顺序重要吗?如果8 移到前面会怎样:[8, 1, 2, 3, 4, 5, 6, 7],订单是否计算在内,或者只有在场/缺席(一组)?列表是否可以包含嵌套字典,而字典又包含列表等?
  • 你能举一个输出失败的例子吗?
  • @samplebias:是的。列表可以包含字典,字典可以包含....它的海龟一路向下。我真的不需要元组,但在这一点上,这并没有多大帮助

标签: python data-structures recursion diff dictionary


【解决方案1】:

如果您想要递归地进行差异化,我已经为 python 编写了一个包: https://github.com/seperman/deepdiff

安装

从 PyPi 安装:

pip install deepdiff

示例用法

导入

>>> from deepdiff import DeepDiff
>>> from pprint import pprint
>>> from __future__ import print_function # In case running on Python 2

相同的对象返回空

>>> t1 = {1:1, 2:2, 3:3}
>>> t2 = t1
>>> print(DeepDiff(t1, t2))
{}

项目类型已更改

>>> t1 = {1:1, 2:2, 3:3}
>>> t2 = {1:1, 2:"2", 3:3}
>>> pprint(DeepDiff(t1, t2), indent=2)
{ 'type_changes': { 'root[2]': { 'newtype': <class 'str'>,
                                 'newvalue': '2',
                                 'oldtype': <class 'int'>,
                                 'oldvalue': 2}}}

物品的价值发生了变化

>>> t1 = {1:1, 2:2, 3:3}
>>> t2 = {1:1, 2:4, 3:3}
>>> pprint(DeepDiff(t1, t2), indent=2)
{'values_changed': {'root[2]': {'newvalue': 4, 'oldvalue': 2}}}

添加和/或删除项目

>>> t1 = {1:1, 2:2, 3:3, 4:4}
>>> t2 = {1:1, 2:4, 3:3, 5:5, 6:6}
>>> ddiff = DeepDiff(t1, t2)
>>> pprint (ddiff)
{'dic_item_added': ['root[5]', 'root[6]'],
 'dic_item_removed': ['root[4]'],
 'values_changed': {'root[2]': {'newvalue': 4, 'oldvalue': 2}}}

字符串区别

>>> t1 = {1:1, 2:2, 3:3, 4:{"a":"hello", "b":"world"}}
>>> t2 = {1:1, 2:4, 3:3, 4:{"a":"hello", "b":"world!"}}
>>> ddiff = DeepDiff(t1, t2)
>>> pprint (ddiff, indent = 2)
{ 'values_changed': { 'root[2]': {'newvalue': 4, 'oldvalue': 2},
                      "root[4]['b']": { 'newvalue': 'world!',
                                        'oldvalue': 'world'}}}

字符串差异2

>>> t1 = {1:1, 2:2, 3:3, 4:{"a":"hello", "b":"world!\nGoodbye!\n1\n2\nEnd"}}
>>> t2 = {1:1, 2:2, 3:3, 4:{"a":"hello", "b":"world\n1\n2\nEnd"}}
>>> ddiff = DeepDiff(t1, t2)
>>> pprint (ddiff, indent = 2)
{ 'values_changed': { "root[4]['b']": { 'diff': '--- \n'
                                                '+++ \n'
                                                '@@ -1,5 +1,4 @@\n'
                                                '-world!\n'
                                                '-Goodbye!\n'
                                                '+world\n'
                                                ' 1\n'
                                                ' 2\n'
                                                ' End',
                                        'newvalue': 'world\n1\n2\nEnd',
                                        'oldvalue': 'world!\n'
                                                    'Goodbye!\n'
                                                    '1\n'
                                                    '2\n'
                                                    'End'}}}

>>> 
>>> print (ddiff['values_changed']["root[4]['b']"]["diff"])
--- 
+++ 
@@ -1,5 +1,4 @@
-world!
-Goodbye!
+world
 1
 2
 End

类型改变

>>> t1 = {1:1, 2:2, 3:3, 4:{"a":"hello", "b":[1, 2, 3]}}
>>> t2 = {1:1, 2:2, 3:3, 4:{"a":"hello", "b":"world\n\n\nEnd"}}
>>> ddiff = DeepDiff(t1, t2)
>>> pprint (ddiff, indent = 2)
{ 'type_changes': { "root[4]['b']": { 'newtype': <class 'str'>,
                                      'newvalue': 'world\n\n\nEnd',
                                      'oldtype': <class 'list'>,
                                      'oldvalue': [1, 2, 3]}}}

列表差异

>>> t1 = {1:1, 2:2, 3:3, 4:{"a":"hello", "b":[1, 2, 3, 4]}}
>>> t2 = {1:1, 2:2, 3:3, 4:{"a":"hello", "b":[1, 2]}}
>>> ddiff = DeepDiff(t1, t2)
>>> pprint (ddiff, indent = 2)
{'iterable_item_removed': {"root[4]['b'][2]": 3, "root[4]['b'][3]": 4}}

列出差异2:

>>> t1 = {1:1, 2:2, 3:3, 4:{"a":"hello", "b":[1, 2, 3]}}
>>> t2 = {1:1, 2:2, 3:3, 4:{"a":"hello", "b":[1, 3, 2, 3]}}
>>> ddiff = DeepDiff(t1, t2)
>>> pprint (ddiff, indent = 2)
{ 'iterable_item_added': {"root[4]['b'][3]": 3},
  'values_changed': { "root[4]['b'][1]": {'newvalue': 3, 'oldvalue': 2},
                      "root[4]['b'][2]": {'newvalue': 2, 'oldvalue': 3}}}

忽略顺序或重复列出差异:(使用与上述相同的字典)

>>> t1 = {1:1, 2:2, 3:3, 4:{"a":"hello", "b":[1, 2, 3]}}
>>> t2 = {1:1, 2:2, 3:3, 4:{"a":"hello", "b":[1, 3, 2, 3]}}
>>> ddiff = DeepDiff(t1, t2, ignore_order=True)
>>> print (ddiff)
{}

包含字典的列表:

>>> t1 = {1:1, 2:2, 3:3, 4:{"a":"hello", "b":[1, 2, {1:1, 2:2}]}}
>>> t2 = {1:1, 2:2, 3:3, 4:{"a":"hello", "b":[1, 2, {1:3}]}}
>>> ddiff = DeepDiff(t1, t2)
>>> pprint (ddiff, indent = 2)
{ 'dic_item_removed': ["root[4]['b'][2][2]"],
  'values_changed': {"root[4]['b'][2][1]": {'newvalue': 3, 'oldvalue': 1}}}

套装:

>>> t1 = {1, 2, 8}
>>> t2 = {1, 2, 3, 5}
>>> ddiff = DeepDiff(t1, t2)
>>> pprint (DeepDiff(t1, t2))
{'set_item_added': ['root[3]', 'root[5]'], 'set_item_removed': ['root[8]']}

命名元组:

>>> from collections import namedtuple
>>> Point = namedtuple('Point', ['x', 'y'])
>>> t1 = Point(x=11, y=22)
>>> t2 = Point(x=11, y=23)
>>> pprint (DeepDiff(t1, t2))
{'values_changed': {'root.y': {'newvalue': 23, 'oldvalue': 22}}}

自定义对象:

>>> class ClassA(object):
...     a = 1
...     def __init__(self, b):
...         self.b = b
... 
>>> t1 = ClassA(1)
>>> t2 = ClassA(2)
>>> 
>>> pprint(DeepDiff(t1, t2))
{'values_changed': {'root.b': {'newvalue': 2, 'oldvalue': 1}}}

添加了对象属性:

>>> t2.c = "new attribute"
>>> pprint(DeepDiff(t1, t2))
{'attribute_added': ['root.c'],
 'values_changed': {'root.b': {'newvalue': 2, 'oldvalue': 1}}}

【讨论】:

  • 感谢@LukasN.P.Egger
  • @MohitC 你能在github上开一张票,写下语法错误在哪里吗?
  • 它在导入行本身。文件“/usr/lib/python2.6/site-packages/deepdiff/__init__.py”,第 1 行,在 从 .deepdiff 导入 DeepDiff 文件“/usr/lib/python2.6/site-packages/deepdiff /deepdiff.py",第 213 行 self.__diff(t1, t2, parents_ids=frozenset({id(t1)})) 我使用的是 python 2.6.6
  • @MohitC 它不兼容 python 2.6。 (在 github repo 的顶部,它说明了它兼容的版本)。你为什么使用 python 2.6?
  • @Seperman 是否有 DeepDiff(t1, t2).is_equal 方法,还是我需要做 str(DeepDiff(t1, t2)) == "{}" ?我只需要知道它们是否相等......
【解决方案2】:

这是一个受Winston Ewert启发的实现

def recursive_compare(d1, d2, level='root'):
    if isinstance(d1, dict) and isinstance(d2, dict):
        if d1.keys() != d2.keys():
            s1 = set(d1.keys())
            s2 = set(d2.keys())
            print('{:<20} + {} - {}'.format(level, s1-s2, s2-s1))
            common_keys = s1 & s2
        else:
            common_keys = set(d1.keys())

        for k in common_keys:
            recursive_compare(d1[k], d2[k], level='{}.{}'.format(level, k))

    elif isinstance(d1, list) and isinstance(d2, list):
        if len(d1) != len(d2):
            print('{:<20} len1={}; len2={}'.format(level, len(d1), len(d2)))
        common_len = min(len(d1), len(d2))

        for i in range(common_len):
            recursive_compare(d1[i], d2[i], level='{}[{}]'.format(level, i))

    else:
        if d1 != d2:
            print('{:<20} {} != {}'.format(level, d1, d2))

if __name__ == '__main__':
    d1={'a':[0,2,3,8], 'b':0, 'd':{'da':7, 'db':[99,88]}}
    d2={'a':[0,2,4], 'c':0, 'd':{'da':3, 'db':7}}

    recursive_compare(d1, d2)

将返回:

root                 + {'b'} - {'c'}
root.a               len1=4; len2=3
root.a[2]            3 != 4
root.d.db            [99, 88] != 7
root.d.da            7 != 3

【讨论】:

  • 如果值是具有不同元素顺序的字典列表,这将失败,不是吗?
【解决方案3】:

一种选择是将您遇到的任何列表转换为以索引为键的字典。例如:

# add this function to the same module
def list_to_dict(l):
    return dict(zip(map(str, range(len(l))), l))

# add this code under the 'if type(d2[k]) == dict' block
                    elif type(d2[k]) == list:
                        dd(list_to_dict(d1[k]), list_to_dict(d2[k]), k)

这是您在 cmets 中提供的示例字典的输出:

>>> d1 = {"name":"Joe", "Pets":[{"name":"spot", "species":"dog"}]}
>>> d2 = {"name":"Joe", "Pets":[{"name":"spot", "species":"cat"}]}
>>> dd(d1, d2, "base")
Changes in base
Changes in Pets
Changes in 0
species changed in d2 to cat
Done with changes in 0
Done with changes in Pets
Done with changes in base

请注意,这将逐个索引进行比较,因此需要进行一些修改才能很好地添加或删除列表项。

【讨论】:

    【解决方案4】:

    只是一个想法:您可以尝试一种面向对象的方法,在该方法中派生自己的字典类,以跟踪对其所做的任何更改(并报告它们)。与尝试比较两个字典相比,这似乎有很多优势……最后会注明一个。

    为了展示如何做到这一点,这里有一个相当完整且经过最少测试的示例实现,它应该适用于 Python 2 和 3:

    import sys
    
    _NUL = object()  # unique object
    
    if sys.version_info[0] > 2:
        def iterkeys(d, **kw):
            return iter(d.keys(**kw))
    else:
        def iterkeys(d, **kw):
            return d.iterkeys(**kw)
    
    
    class TrackingDict(dict):
        """ Dict subclass which tracks all changes in a _changelist attribute. """
        def __init__(self, *args, **kwargs):
            super(TrackingDict, self).__init__(*args, **kwargs)
            self.clear_changelist()
            for key in sorted(iterkeys(self)):
                self._changelist.append(AddKey(key, self[key]))
    
        def clear_changelist(self):  # additional public method
            self._changelist = []
    
        def __setitem__(self, key, value):
            modtype = ChangeKey if key in self else AddKey
            super(TrackingDict, self).__setitem__(key, value)
            self._changelist.append(modtype(key, self[key]))
    
        def __delitem__(self, key):
            super(TrackingDict, self).__delitem__(key)
            self._changelist.append(RemoveKey(key))
    
        def clear(self):
            deletedkeys = self.keys()
            super(TrackingDict, self).clear()
            for key in sorted(deletedkeys):
                self._changelist.append(RemoveKey(key))
    
        def update(self, other=_NUL):
            if other is not _NUL:
                otherdict = dict(other)  # convert to dict if necessary
                changedkeys = set(k for k in otherdict if k in self)
                super(TrackingDict, self).update(other)
                for key in sorted(iterkeys(otherdict)):
                    if key in changedkeys:
                        self._changelist.append(ChangeKey(key, otherdict[key]))
                    else:
                        self._changelist.append(AddKey(key, otherdict[key]))
    
        def setdefault(self, key, default=None):
            if key not in self:
                self[key] = default  # will append an AddKey to _changelist
            return self[key]
    
        def pop(self, key, default=_NUL):
            if key in self:
                ret = self[key]  # save value
                self.__delitem__(key)
                return ret
            elif default is not _NUL:  # default specified
                return default
            else:  # not there & no default
                self[key]  # allow KeyError to be raised
    
        def popitem(self):
            key, value = super(TrackingDict, self).popitem()
            self._changelist.append(RemoveKey(key))
            return key, value
    
    # change-tracking record classes
    
    class DictMutator(object):
        def __init__(self, key, value=_NUL):
            self.key = key
            self.value = value
        def __repr__(self):
            return '%s(%r%s)' % (self.__class__.__name__, self.key,
                                 '' if self.value is _NUL else ': '+repr(self.value))
    
    class AddKey(DictMutator): pass
    class ChangeKey(DictMutator): pass
    class RemoveKey(DictMutator): pass
    
    if __name__ == '__main__':
        import traceback
        import sys
    
        td = TrackingDict({'one': 1, 'two': 2})
        print('changelist: {}'.format(td._changelist))
    
        td['three'] = 3
        print('changelist: {}'.format(td._changelist))
    
        td['two'] = -2
        print('changelist: {}'.format(td._changelist))
    
        td.clear()
        print('changelist: {}'.format(td._changelist))
    
        td.clear_changelist()
    
        td['newkey'] = 42
        print('changelist: {}'.format(td._changelist))
    
        td.setdefault('another') # default None value
        print('changelist: {}'.format(td._changelist))
    
        td.setdefault('one more', 43)
        print('changelist: {}'.format(td._changelist))
    
        td.update(zip(('another', 'one', 'two'), (17, 1, 2)))
        print('changelist: {}'.format(td._changelist))
    
        td.pop('newkey')
        print('changelist: {}'.format(td._changelist))
    
        try:
            td.pop("won't find")
        except KeyError:
            print("KeyError as expected:")
            traceback.print_exc(file=sys.stdout)
        print('...and no change to _changelist:')
        print('changelist: {}'.format(td._changelist))
    
        td.clear_changelist()
        while td:
            td.popitem()
        print('changelist: {}'.format(td._changelist))
    

    注意,与字典的 beforeafter 状态的简单比较不同,该类将告诉您添加的键然后删除——换句话说,它会保留完整的历史记录,直到其 _changelist 被清除。

    输出:

    changelist: [AddKey('one': 1), AddKey('two': 2)]
    changelist: [AddKey('one': 1), AddKey('two': 2), AddKey('three': 3)]
    changelist: [AddKey('one': 1), AddKey('two': 2), AddKey('three': 3), ChangeKey('two': -2)]
    changelist: [AddKey('one': 1), AddKey('two': 2), AddKey('three': 3), ChangeKey('two': -2), RemoveKey('one'), RemoveKey('three'), RemoveKey('two')]
    changelist: [AddKey('newkey': 42)]
    changelist: [AddKey('newkey': 42), AddKey('another': None)]
    changelist: [AddKey('newkey': 42), AddKey('another': None), AddKey('one more': 43)]
    changelist: [AddKey('newkey': 42), AddKey('another': None), AddKey('one more': 43), ChangeKey('another': 17), AddKey('one': 1), AddKey('two': 2)]
    changelist: [AddKey('newkey': 42), AddKey('another': None), AddKey('one more': 43), ChangeKey('another': 17), AddKey('one': 1), AddKey('two': 2), RemoveKey('newkey')]
    KeyError as expected:
    Traceback (most recent call last):
      File "trackingdict.py", line 122, in <module>
        td.pop("won't find")
      File "trackingdict.py", line 67, in pop
        self[key]  # allow KeyError to be raised
    KeyError: "won't find"
    ...and no change to _changelist:
    changelist: [AddKey('newkey': 42), AddKey('another': None), AddKey('one more': 43), ChangeKey('another': 17), AddKey('one': 1), AddKey('two': 2), RemoveKey('newkey')]
    changelist: [RemoveKey('one'), RemoveKey('two'), RemoveKey('another'), RemoveKey('one more')]
    

    【讨论】:

      【解决方案5】:

      您的函数应该首先检查其参数的类型,编写函数以便它可以处理列表、字典、整数和字符串。这样您就不必复制任何内容,只需递归调用即可。

      伪代码:

      def compare(d1, d2):
           if d1 and d2 are dicts
                  compare the keys, pass values to compare
           if d1 and d2 are lists
                  compare the lists, pass values to compare
           if d1 and d2 are strings/ints
                  compare them
      

      【讨论】:

        【解决方案6】:

        正如 Serge 所建议的那样,我发现这个解决方案有助于快速获得关于两个字典是否匹配“一直向下”的布尔返回:

        import json
        
        def match(d1, d2):
            return json.dumps(d1, sort_keys=True) == json.dumps(d2, sort_keys=True)
        

        【讨论】:

          【解决方案7】:

          在递归对象时考虑使用hasattr(obj, '__iter__')。如果一个对象实现了__iter__ 方法,您就知道可以对其进行迭代。

          【讨论】:

            【解决方案8】:

            自己动手实践和学习很有趣,但我发现对于不平凡的任务,现成和维护的包通常效果更好。

            考虑转换为 json 并使用一些像样的“语义”json 比较器,例如 https://www.npmjs.com/package/compare-json 或在线 http://jsondiff.com。需要字符串化数字键。

            如果你真的需要,可以尝试将 jsondiff 翻译成 python。

            Conversion from JavaScript to Python code?

            【讨论】:

              【解决方案9】:

              你可以试试下面的简单实现

              def recursive_compare(obj1, obj2):
              """ Compare python objects recursively, support type:
              "int, float, long, basestring, set, datetime, date, dict, Sequence"
              
              Example:
              >>> recursive_compare([1, 2, 3], [1, 2, 3])
              >>> True
              >>> recursive_compare([1, 2, 3], [1, 2, 4])
              >>> False
              >>> recursive_compare({'a': 1}, {'a': 2})
              >>> False
              """
              
              def _diff(obj1, obj2):
                  # exclude type basestring for backward-compatible python2:
                  # <str, unicode>
                  if type(obj1) != type(obj2) and not isinstance(obj1, basestring):
                      return False
              
                  elif isinstance(obj1,
                                  (int, float, long, basestring, set, datetime, date)):
                      if obj1 != obj2:
                          return False
              
                  elif isinstance(obj1, dict):
                      keys = obj1.viewkeys() & obj2.viewkeys()
                      if obj1 and len(keys) == 0 \
                          or keys.difference(set(obj1.keys())) \
                              or keys.difference(set(obj2.keys())):
                          return False
              
                      for k in keys:
                          if _diff(obj1[k], obj2[k]) is False:
                              return False
              
                  elif isinstance(obj1, collections.Sequence):
                      # require sorted sequence object
                      if len(obj1) != len(obj2):
                          return False
              
                      for i in range(len(obj1)):
                          if _diff(obj1[i], obj2[i]) is False:
                              return False
              
                  else:
                      raise TypeError('do not support type {} to compare'.format(
                          type(obj1)))
              
              return False if _diff(obj1, obj2) is False else True
              

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 2021-05-22
                • 1970-01-01
                • 2010-11-13
                • 2021-06-29
                • 1970-01-01
                • 2018-03-10
                • 2021-04-10
                相关资源
                最近更新 更多