Welcome, guest | Sign In | My Account | Store | Cart

Set-like operations for dictionaries

Python, 47 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class setdict(dict):
    '''
    Add set operations to dicts.
    '''
    def __sub__(self, other):
        res = {}
        for k in set(self) - set(other):
            res[k] = self[k]
        return setdict(**res)
    
    def __and__(self, other):
        res = {}
        for k in set(self) & set(other):
            res[k] = self[k]
        return setdict(**res)
        
    def __xor__(self, other):
        res = {}
        for k in set(self) ^ set(other):
            try:
                res[k] = self[k]
            except KeyError:
                res[k] = other[k]
        return setdict(**res)
    
    def __or__(self, other):
        res = {}
        for k in set(self) | set(other):
            try:
                res[k] = self[k]
            except KeyError:
                res[k] = other[k]
        return setdict(**res)

def call_with_filtered_args(args, _callable):
    '''
    Filter any nonkeyword elements from args, then call
    the callable with them.
    '''
    try:
        argnames = _callable.func_code.co_varnames
    except AttributeError:
        argnames = _callable.__init__.func_code.co_varnames

    args = setdict(**args) & argnames
    
    return _callable(**args)

To merge two dicts, there is dict.update, but that only provides a solution for when you need what amounts to a set union between the keys of each dict. Yet there are situations where a set intersection, difference, or symetric difference would also be useful to perform in a terse, pythonic way. The code below presents a way to do those things with operator overloading.

One example is when you're calculating specific values to pass to another function and would simply find it easier to call the function with by unpacking locals() in the function call, except for the fact that any non-keyword arguments in locals() will cause an error:

>>> a = 1
>>> b = 3
>>> junk1 = 3
>>> junk2 = 4
>>> def f(a,b):
...     return a * b
...
>>> 
>>> f(**locals())

Traceback (most recent call last):
  File "<pyshell#18>", line 1, in <module>
    f(**locals())
TypeError: f() got an unexpected keyword argument 'junk2'

Using setdict you can perform a set intersection on the keys in locals() and the varnames for a function and thereby filter out any keys that aren't in the args list for the function:

>>> args = setdict(**locals()) & f.func_code.co_varnames
>>> f(**args)
>>> 3

This provides a handy abstraction for calling functions with locals() where locals may be littered with various junk. See the function call_with_filtered_args below for a shortcut.

Here are some other uses of setdict:

>>> dict1 = setdict(**{1:2, 3:4, 5:6})
>>> dict2 = setdict(**{3:4, 4:5, 5:6})
>>> dict3 = setdict(**{4:5, 6:7, 5:6})
>>> dict4 = setdict(**{5:8, 6:9, 7:10})

>>> dicts = [dict1, dict2, dict3, dict4]

>>> # intersection
>>> dict1 & dict2
{3: 4, 5: 6}

>>> # difference
>>> dict1 - dict2
{1: 2}

>>> # union
>>> dict1 | dict2
{1: 2, 3: 4, 4: 5, 5: 6}

>>> # intersection
>>> dict1 ^ dict2
{1: 2, 4: 5}

>>> # symetric difference
>>> reduce(operator.and_, dicts)
{5: 6}

>>> # reduce or
>>> reduce(operator.or_, dicts)
{1: 2, 3: 4, 4: 5, 5: 6, 6: 7, 7: 10}

>>> # reduce xor
>>> reduce(operator.xor, dicts)
{1: 2, 7: 10}

>>> # reduce and
>>> reduce(operator.and_, dicts)
{5: 6}

The last example can be used to perform a simple inheritance operation for a sequence of dicts, with precedence being accorded from left to right.

1 comment

Raymond Hettinger 13 years, 4 months ago  # | flag

FWIW, set operations on dictionaries are already part of the Python core distribution as of Py2.7 and Py3.0.

Created by thom neale on Sat, 27 Nov 2010 (MIT)
Python recipes (4591)
thom neale's recipes (3)

Required Modules

  • (none specified)

Other Information and Tasks