ratherlargeguy
ratherlargeguy

Reputation: 41

How should I deal with this recursive behaviour?

Suppose I had a class 'A' and nested within 'A' was class 'B' and both objects were for use publicly...

class A(object):

    class B(object):

        def __init__(self):
            self._b = dict()
            self.a = A()

        def __setitem__(self, key, value):
            # set self._b
            self.a[key] = value  # recursion ??

    def __init__(self):
        self._a = dict()
        self.b = A.B()

    def __setitem__(self, key, value):
        # set self._a
        self.b[key] = value     # recursion ??

Now when doing...

d = A()
d['sap'] = 'nin'

A.__setitem__ will call B.__setitem__, which will call A.__setitem__ and so on...

If this is the desired interface, what would be the best way to deal with this recursive behaviour?

for more context, I've posted on codereview here...

https://codereview.stackexchange.com/questions/85842/a-dictionary-that-allows-multiple-keys-for-one-value

I've tried using another nested class to pass to other internal functions so that they can determine whether it was called from the user, or internally from other functions...

class A(object):

    class _Container(object):
        def __init__(self, _object):
            self.object = _object

    class B(object):
        def __init__(self, d, **kwargs):
            if is instance(d, A._Container):
                self.a = d.object
            else:
                self.a = A(A._Container(self))

    def __init__(self, d, **kwargs):
        if isinstance(d, A._Container):
            self.b = d.object
        else:
            self.b = A.B(A._Container(self))

This works for init, but I'm not sure I'd like to do that with setitem and all other methods.

I've also tried delegating all or most of the work to class 'A' so essentially class 'B''s methods act as hooks...

class A(object):
    class B(object):
        def __setitem__(self, key, value):
            # some functionality
            self.a[key] = value

    def __setitem__(self, key, value):
        self._a[key] = value
        self._b[key] = value

If it helps, here is the code I'm working on atm.

# -*- coding: utf-8 -*-

class mkdict(object):
    """ A dictionary that allows multiple keys for one value """


    class _Container(object):
        """ This is used to wrap an object to avoid infinite
        recursion when calling my own methods from the inside.

        If a method sees this container, it assumes it has been
        called from the inside and not the user.
        """

        def __init__(self, _object):
            self.object = _object


    class dict(object):
        """ Interface for mkdict.dict for dict-like behaviour """

        def __init__(self, d={}, **kwargs):
            """ Using an mkdict._Container to avoid infinite
            recursion when allowing:

            >>> d = mkdict({'what': 'ever'})
            >>> d = mkdict.dict({'what': 'ever'})
            """
            if isinstance(d, mkdict._Container):
                self.mkdict = d.object
            else:
                self.mkdict = mkdict(mkdict._Container(self))
                self.update(d, **kwargs)

        def __str__(self):
            return str(self.mkdict._dict)

        def __repr__(self):
            return str(self)

        def __len__(self):
            return len(self.mkdict._dict)

        def __setitem__(self, key, value):
            """ Desired behaviour:

            >>> d = mkdict()
            >>>
            >>> d['what', 'ever'] = 'testing'
            >>> d
            {'what': 'testing', 'ever': 'testing'}
            >>> d.dict
            {('what', 'ever'): 'testing'}
            >>> d['what'] is d['ever']
            True
            >>>
            >>> d.dict['what'] = 'new value'
            >>> d
            {'what': 'new value', 'ever': 'testing'}
            >>> d.dict
            {'what': 'new value', 'ever': 'testing'}
            >>> d['what'] is d['ever']
            False
            """
            if key not in self and key in self.mkdict:
                self.mkdict._key_already_set(key)

            self.mkdict[key] = value

        def __getitem__(self, key):
            return self.mkdict._dict[key]

        def __contains__(self, key):
            return key in self.mkdict._dict

        def __delitem__(self, key):
            if key not in self:
                raise KeyError(key)

            if isinstance(key, tuple):
                key = key[0]

            del self.mkdict[key]

        def clear(self):
            self.mkdict.clear()

        def update(self, d, **kwargs):
            if isinstance(d, mkdict.dict):
                d = d.mkdict._dict
            elif isinstance(d, mkdict):
                d = d._dict

            d.update(kwargs):
            for k, v in d.items():
                self[k] = v


    class _FullKeyPtr(object):
        """ Desired behaviour:

        full_key_ptr1 = _FullKeyPtr()
        mkdict._key_map -> {'key1', full_key_ptr1,
                            'key2', full_key_ptr1}

        >>> d = mkdict()
        >>> d['what', 'ever'] = 'testing'
        >>> d._key_map
        >>>
        >>> # d._key_map:
        >>> # {'what': full_key_ptr1, 'ever': full_key_ptr1}
        >>> d._key_map
        >>> {'what': ('what', 'ever'), 'ever': ('what', 'ever')}
        >>>
        >>> d['what']
        >>> 'testing'
        >>>
        >>> # full_key = _key_map['ever'].full_key
        >>> # i.e. full_key = ('what', 'ever')
        >>> # _dict[full_key] = 'test'
        >>> d['ever'] = 'test'
        >>>
        >>>
        >>> d['what']
        >>> 'test'
        """

        def __init__(self, full_key):
            self.full_key = full_key

        def __str__(self):
            return str(self.full_key)

        def __repr__(self):
            return str(self)


    def __init__(self, d={}, **kwargs):
        self._dict = dict()
        self._key_map = dict()
        self._dict_backup = None
        self._key_map_backup = None

        if isinstance(d, mkdict._Container):
            self.dict = d.object
        else:
            self.dict = mkdict.dict(mkdict._Container(self))
            self.update(d, **kwargs)

    def __str__(self):
        return str(dict(self.items()))

    def __repr__(self):
        return str(self)

    def __len__(self):
        return len(self._key_map)

    def __iter__(self):
        return iter(self.keys())

    def __getitem__(self, key):
        full_key = self.full_key(key)
        return self.dict[full_key]

    def __setitem__(self, key, value):
        """ Desired behaviour:

        >>> d = mkdict()
        >>> d['what', 'ever'] = 'testing'
        >>>
        >>> d
        {'what': 'testing', 'ever': 'testing'}
        >>>
        >>> d.dict
        {('what', 'ever'): 'testing'}
        >>> d['what'] is d['ever']
        True
        >>>
        >>> d['what'] = 'new value'
        >>> d
        {'what': 'new value', 'ever': 'new value'}
        >>>
        >>> d.dict
        {('what', 'ever'): 'new value'}
        >>> d['what'] is d['ever']
        True
        """
        if key in self:
            key = self.full_key(key)

        if key not in self._dict:
            if isinstance(key, tuple):
                full_key_ptr = self._FullKeyPtr(key)
                for k in key:
                    if k in self:
                        self._key_already_set(k)
                    self._key_map[k] = full_key_ptr
            else:
                self._key_map[key] = self._FullKeyPtr(key)

        self._dict[key] = value

    def __delitem__(self, key):
        full_key = self.full_key(key)

        if isinstance(full_key, tuple):
            for k in full_key:
                del self._key_map[k]
        else:
            del self._key_map[full_key]

        del self._dict[full_key]

    def __contains__(self, key):
        return key in self._key_map

    def items(self):
        return [(k, self[k]) for k, v in self._key_map.items()]

    def iteritems(self):
        return iter(self.items())

    def update(self, d={}, **kwargs):
        if isinstance(d, mkdict.dict):
            d = d.mkdict._dict
        elif isinstance(d, mkdict):
            d = d._dict

        d.update(kwargs)
        for k, v in d.items():
            self[k] = v

    def clear(self):
        self._dict.clear()
        self._key_map.clear()

    def keys(self):
        return self._key_map.keys()

    def full_key(self, key):
        return self._key_map[key].full_key

    def has_key(self, key):
        return key in self

    def append(self, key, otherkey):
        pass

    def remove(self, key):
        full_key = self.full_key(key)

        if not isinstance(full_key, tuple):
            del self._dict[full_key]
            del self._key_map[full_key]
            return

        new_full_key = list(full_key)
        new_full_key.remove(key)

        if len(new_full_key) == 1:
            new_full_key = new_full_key[0]
        else:
            new_full_key = tuple(new_full_key)

        self._dict[new_full_key] = self.dict[full_key]
        del self._dict[full_key]
        self._key_map[key].full_key = new_full_key
        del self._key_map[key]

    def aliases(self, key):
        full_key = self.full_key(key)
        if isinstance(full_key, tuple):
            aliases = list(full_key)
            aliases.remove(key)
            return aliases
        return list()

    def backup(self):
        pass

    def revert(self):
        pass

    def _key_already_set(self, key):
        self.remove(key)

Desired behaviour for above code:

>>> d = mkdict()
>>>
>>> d['-p', '--port'] = 1234
>>> d
{'-p': 1234, '--port': 1234}
>>> d.dict
{('-p', '--port'): 1234}
>>>
>>> d['-p'] = 5678
>>> d
{'-p': 5678, '--port': 5678}
>>> d['--port'] is d['-p']
True
>>> d.aliases('-p')
['--port']
>>>
>>> d.dict['-p'] = 1234
>>> d
{'-p': 1234, '--port': 5678}
>>> d.dict
{'-p': 1234, '--port': 5678}
>>>
>>> d['-p'] is d['--port']
False

Upvotes: 0

Views: 153

Answers (1)

pnv
pnv

Reputation: 3145

I've come up with an alternative solution, which is not so elegant (it's a hack), but should work as you've said. It overrides getitem and setitem, and for tuple like key representation, the minimal_get function is added.

from operator import itemgetter
from itertools import groupby
import logging


class mydict(dict):

    def __init__(self, **kwargs):
        super(mydict, self).__init__(**kwargs)
        # self.d = {}

    def __getitem__(self, item):
        if isinstance(item, tuple):
            d = {}
            for an_item in item:
                d[an_item] = self.__getitem__(an_item)
            return d
        else:
            return super(mydict, self).__getitem__(item)

    def __setitem__(self, keys, value, _depth=0):
        if isinstance(keys, tuple) and _depth == 0:
            if isinstance(value, tuple):
                if len(keys) == len(value):
                    pass
                else:
                    value = len(keys) * (value,)
            else:
                value = len(keys) * (value,)
            for an_item in zip(keys, value):
                self.__setitem__(an_item[0], an_item[1], _depth=1)
        else:
            super(mydict, self).__setitem__(keys, value)

    def minimal_get(self):
        x = {}
        for item in groupby(sorted(self.items(), key=itemgetter(1)), key=itemgetter(1)):
            keys = []
            try:
                while item[1]:
                    keys.append(item[1].next()[0])
            except StopIteration:
                logging.info("StopIteration")
            x[tuple(keys)] = item[0]
        return x


dic = mydict()
dic["one"] = 1
dic["seven"] = 2
print "dic is", dic
dic["two", "three", "four"] = [1, 2, 3]
print "dic.minimal_get() is ", dic.minimal_get()
print "dic is", dic
dic["two", "ten"] = "lol"
print "dic[\"one\", \"two\"] is ", dic["one", "two"]
print "dic[\"three\"] is", dic["three"]
print "dic.minimal_get() is ", dic.minimal_get()
dic[("x", "z",), "h"] = (1, 2, 3)
print "dic is", dic
print "dic.minimal_get() is ", dic.minimal_get()

Result is,

Terminal screen-shot

Upvotes: 1

Related Questions