How do I do advanced Python hash autovivification?

前端 未结 4 493
春和景丽
春和景丽 2021-01-02 04:30

This question is about implementing the full Perl autovivification in Python. I know similar questions were asked before and so far the best answer is in \"What is the best

相关标签:
4条回答
  • 2021-01-02 05:14

    Since we don't know beforehand if we need a dictionary, or a list, then you can't combine autovivification with lists. Unless, Working off of Nosklo's answer from the linked question, you add list "features" to the underlying dictionary. Basically assuming a "sort" order for keys, and always using it with the list methods. I've done this as an example:

    class AutoVivification(dict):
        """Implementation of perl's autovivification feature. Has features from both dicts and lists,
        dynamically generates new subitems as needed, and allows for working (somewhat) as a basic type.
        """
        def __getitem__(self, item):
            if isinstance(item, slice):
                d = AutoVivification()
                items = sorted(self.iteritems(), reverse=True)
                k,v = items.pop(0)
                while 1:
                    if (item.start < k < item.stop):
                        d[k] = v
                    elif k > item.stop:
                        break
                    if item.step:
                        for x in range(item.step):
                            k,v = items.pop(0)
                    else:
                        k,v = items.pop(0)
                return d
            try:
                return dict.__getitem__(self, item)
            except KeyError:
                value = self[item] = type(self)()
                return value
    
        def __add__(self, other):
            """If attempting addition, use our length as the 'value'."""
            return len(self) + other
    
        def __radd__(self, other):
            """If the other type does not support addition with us, this addition method will be tried."""
            return len(self) + other
    
        def append(self, item):
            """Add the item to the dict, giving it a higher integer key than any currently in use."""
            largestKey = sorted(self.keys())[-1]
            if isinstance(largestKey, str):
                self.__setitem__(0, item)
            elif isinstance(largestKey, int):
                self.__setitem__(largestKey+1, item)
    
        def count(self, item):
            """Count the number of keys with the specified item."""
            return sum([1 for x in self.items() if x == item])
    
        def __eq__(self, other):
            """od.__eq__(y) <==> od==y. Comparison to another AV is order-sensitive
            while comparison to a regular mapping is order-insensitive. """
            if isinstance(other, AutoVivification):
                return len(self)==len(other) and self.items() == other.items()
            return dict.__eq__(self, other)
    
        def __ne__(self, other):
            """od.__ne__(y) <==> od!=y"""
            return not self == other
    

    This follows the basic autovivification feature of dynamically generating itself for dud keys. However, it also implements some of the methods listed here. This allows it to act like a quasi list/dict thing.

    For the rest of the list features, add in the listed methods. I'm treating it as a dictionary with the list methods. If a list method is called, then it makes an assumption about the order of the items held, namely that strings sort lower than integers, and that keys are always in "sorted" order.

    It also supports adding, as an example of these methods. This comes from my own use case. I needed to add items from a AutoVivified dictionary, but if it doesn't exist, a new AutoVivification object is created and returned. They have no integer "value" and so you can't do this:

    rp = AutoVivification()
    rp['a']['b'] = 3
    rp['a']['b'] + rp['q']
    

    That defeats the purpose, since I don't know if some thing is going to be there, but I want a default anyway. So I've added the __add__ and __radd__ methods to it. They use the length of the underlying dictionary as the integer value, so a newly created AV object has a value of zero for addition. If a key has something besides an AV object in it, then we get that thing's addition method, if implemented.

    0 讨论(0)
  • 2021-01-02 05:21

    Just expanding on Ignacio's answer to present some additional options that allow you to explicitly request more magical behaviour from Python dictionaries. The maintainability of code written this way remains dubious, but I wanted to make it crystal clear that it is a question of "Is this kind of data structure maintainable?" (I have my doubts) not "Can Python be made to behave this way?" (it certainly can).

    To support arbitrary levels of nesting for the namespace aspect, all you have to do is name the function (instead of using lambda) and make it self-referential:

    >>> from collections import defaultdict
    >>> def autodict(): return defaultdict(autodict)
    ...
    >>> a = autodict()
    >>> a[1][2][3] = []
    >>> type(a[1])
    <class 'collections.defaultdict'>
    >>> type(a[1][2])
    <class 'collections.defaultdict'>
    >>> type(a[1][2][3])
    <class 'list'>
    >>> a[1][2][3]
    []
    

    However, this introduces the "problem" that you have to set a list explicitly before you can append to it. Python's answer to that lies in the setdefault method, which has actually been around longer than collections.defaultdict:

    >>> a.setdefault(3, []).append(10)
    >>> a.setdefault(3, []).append(11)
    >>> a[3]
    [10, 11]
    >>> a[2].setdefault(3, []).append(12)
    >>> a[2].setdefault(3, []).append(13)
    >>> a[2][3]
    [12, 13]
    >>> a[1][2].setdefault(3, []).append(14)
    >>> a[1][2].setdefault(3, []).append(15)
    >>> a[1][2][3]
    [14, 15]
    

    All collections.defaultdict really does is make the common case where you always pass the same second parameter to dict.setdefault much easier to use. For more complex cases, such as this one, you can still use dict.setdefault directly for the aspects that collections.defaultdict can't handle.

    0 讨论(0)
  • 2021-01-02 05:26
    a = collections.defaultdict(lambda: collections.defaultdict(list))
    
    0 讨论(0)
  • 2021-01-02 05:27

    Perhaps this solves your need for any number of “dimensions” in your dictionary:

    a= collections.defaultdict(list)
    

    the only change in your code being:

    a['x', 'y'].append('z')
    

    Of course, this solution being the one you want depends on two conditions:

    1. whether you need to easily access all lists with e.g. 'x' in the “first dimension“
    2. whether you are stuck to the way Perl magically pleases you more :)

    If either of these two conditions is true, my solution won't help you.

    0 讨论(0)
提交回复
热议问题