4

Python allows dictionaries to be compared with ==

import copy

child = {'name': 'child'}
parent_1 = {'name': 'parent', 'child': child}
parent_2 = copy.deepcopy(parent_1)
print(parent_1 == parent_2)

Prints True, as you would expect it to.

Python also allows dictionaries to reference each other circularly.

child = {'name': 'child'}
parent_1 = {'name': 'parent', 'child': child}
child['parent'] = parent_1  # Create the circular reference

However, trying to use the == operator on dictionaries with circular references raises an error.

parent_2 = copy.deepcopy(parent_1)
print(parent_1 == parent_2)

Returns

C:\Python34\python.exe -i C:/Users/anon/.PyCharm40/config/scratches/scratch_5
Traceback (most recent call last):
  File "C:/Users/anon/.PyCharm40/config/scratches/scratch_5", line 11, in <module>
    print(parent_1 == parent_2)
RuntimeError: maximum recursion depth exceeded in comparison

How can I check two dictionaries with circular references for equality?

MackM
  • 2,906
  • 5
  • 31
  • 45

4 Answers4

3

You need to define what you mean by equal. Normally "equal" for dictionaries means 'all the key/value pairs are the "equal"'. If a dictionary has a reference to itself, this definition of equal may lead to a recursive definition, i.e. a == b iff a == b.

Take this simple example:

a = {}; a['item'] = a
b = {}; b['item'] = b

Are a and b equal? In order to know that, you need to first know if a and b are equal ...

You could create a special equal that looks something like this:

def equal(a, b, special=[]):
    if not isinstance(a, dict) or not isinstance(b, dict):
        return a == b

    special = special + [a, b]
    set_keys = set(a.keys())
    if set_keys != set(b.keys()):
        return False

    for key in set_keys:
        if any(a[key] is i for i in special):
            continue
        elif any(b[key] is i for i in special):
            continue
        elif not equal(a[key], b[key], special):
            return False
    return True
Bi Rico
  • 25,283
  • 3
  • 52
  • 75
  • Your solution meets my example above, but what about cases such as `dict_1 = {'name': '1', 'children': [{'name': '2', 'parent': dict_1}]}`? I'm hoping for a clean solution to fully iterating through various nested inalterables while avoiding circular loops. – MackM Jul 14 '15 at 20:41
  • 1
    This treats all recursive links as being equal, which doesn't make sense. For example, if we make a = {'x':a}, b = {'x':a, 'y':1}, and c = {'x':{'x':c}, 'y':1}, then this equal procedure says that b and c are equal, when they are not. c has y's all the way down, but b does not. – Andru Luvisi Jul 14 '15 at 20:50
  • Not knowing the specifics of the OPs full problem, I leave the implementation of the correct definition of "equal" up to OP (or another enthusiastic user). This example is meant to just show that you need to special case recursive references. – Bi Rico Jul 14 '15 at 20:53
  • The most obvious meanings of "equal" would be "the same object" or "the same structure." Assuming that all recursive links are "equal" to all other recursive links doesn't match any obvious meaning of the word "equal." – Andru Luvisi Jul 15 '15 at 17:05
  • @Glomek the most obvious meaning of "equal" doesn't work, that's why we're in this situation in the first place :). Feel free to write another answer or edit my answer if you think I have not provided a useful response to the question. – Bi Rico Jul 15 '15 at 20:20
  • I did. See my proposal from yesterday. My proposal is to keep two stacks, and to consider two recursive references to be equal if they both match at the same depth in their respective stacks. – Andru Luvisi Jul 15 '15 at 23:51
0

You need to write your own comparison procedure that takes four arguments, two things to compare, and two stacks of dictionaries/lists (one for each thing being compared).

If either thing being compared is contained in the corresponding stack, return true if they are both in their own stack in the same position, and false otherwise.

Otherwise, if they are dictionaries, make sure that they have the same sets of keys (otherwise return false), and recurse on each value, adding the dictionaries to the two stacks.

If they are lists, make sure they are the same length (or return false) and recurse on each pair of members, adding the lists to the two stacks.

To get things started, call the recursive procedure with the things being compared and two empty stacks. You can wrap this in another procedure that only takes two arguments.

def my_compare(a, b):
    return my_compare_helper(a, b, [], [])

def my_index(thing, stack):
    for i in range(len(stack)):
        if thing is stack[i]:
            return i
    return -1

def my_compare_helper(a, b, a_stack, b_stack):
    a_loc = my_index(a, a_stack)
    b_loc = my_index(b, b_stack)
    if a_loc != -1 or b_loc != -1:
        return a_loc == b_loc

    a_stack = [a] + a_stack
    b_stack = [b] + b_stack

    if isinstance(a, list):
        if not isinstance(b, list) or len(a) != len(b):
            return False
        for a_thing, b_thing in zip(a, b):
            if not my_compare_helper(a_thing, b_thing, a_stack, b_stack):
                return False
        return True

    if isinstance(a, dict):
        if not isinstance(b, dict):
            return False
        a_keys = sorted(a.keys())
        b_keys = sorted(b.keys())
        if a_keys != b_keys:    # Keys can't be recursive.
            return False
        for key in a_keys:
            if not my_compare_helper(a[key], b[key], a_stack, b_stack):
                return False
        return True

    return a == b

Sample usage:

>>> a = [1, 2, {}]
>>> a[1] = a
>>> a[2]['x'] = a
>>> b = [1, 2, {}]
>>> b[1] = b
>>> b[2]['x'] = b
>>> my_compare(a, b)
True
>>> 
Andru Luvisi
  • 24,367
  • 6
  • 53
  • 66
0

It occurred to me while I was doing something else that the pickle module handles recursive dictionaries and such. With that in mind this might work for you:

import copy
import cPickle

a = {}
a['self'] = a
a['list'] = [a, a, 0]

b = copy.deepcopy(a)
print(cPickle.dumps(a) == cPickle.dumps(b))
# True

b['self'] = copy.deepcopy(b)
print(cPickle.dumps(a) == cPickle.dumps(b))
# False
Bi Rico
  • 25,283
  • 3
  • 52
  • 75
-1
import copy

child = {'name': 'child'}
parent_1 = {'name': 'parent', 'child': child}
child['parent'] = parent_1  # Create the circular reference

parent_2 = copy.copy(parent_1)
print(parent_1 == parent_2)

If you use copy.copy instead of copy.deepcopy, it runs without an error.

The difference between shallow and deep copying is only relevant for compound objects (objects that contain other objects, like lists or class instances):

• A shallow copy constructs a new compound object and then (to the extent possible) inserts references into it to the objects found in the original.

• A deep copy constructs a new compound object and then, recursively, inserts copies into it of the objects found in the original.

https://docs.python.org/2/library/copy.html

It is maybe a storage issue, that using deepcopy() forces actual recursion, whereas copy() can just check the references

Sam Cohen-Devries
  • 2,025
  • 2
  • 18
  • 35
  • While this does run without error, that is because both parents point to the same child object instead of two different but equivalent child objects. Using a shallow copy, `parent_1['child'] is parent_2['child']` returns `True`, when it should return `False`. Effectively, after the first layer of depth, you are checking an object for equality against itself, and I need to check for equality against a different but equal object. – MackM Jul 14 '15 at 20:06