Pythonでmultimap(値が複数ある辞書)


先日、M.U.G.E.N のstファイルというものを取り扱うソフトを作りました。

stファイルはiniファイルに似ているのですが、

たま〜〜〜〜〜〜に、同じキーに複数回値が設定される事があります。

[state 900]
type = varset
var(10) == 20
var(11) == 30
var(15) == 40



そこでMultiDictを作ってみました。

C++のstd::multidictの同類です。



実際のstファイルではほとんどの場合、
1つのキーには1つの値が対応する(iniと同じ)ので、
普通のdictに変換したり、1つの値が対応している事を確認して取り出すメソッドを付けています。




UserDict.DictMixinを使って実装しています。



def_deligatorsは、単にself._dictに委譲するだけのメソッドを定義するための関数で、
Rubyのそれの真似です。
ちょっと黒魔術風かもしれません。


def_deligators以外は普通だと思います。

#multidict.py
"""
A Implementation of "MultiDict".
MultiDict in this module has novel methods - get1, pop1, asdict1 and ***_m.
"""
import itertools
from UserDict import DictMixin

def def_deligators(locals, member_name, method_names):
    """
        def_deligators(locals, member_name, method_names)
        
        define methods that merely deligate to member.
    """
    
    for method_name in method_names:
        ss = "def %s(self, *a, **kw):return self.%s.%s(*a, **kw)"%(
                method_name, member_name, method_name)
        exec ss in {}, locals

def throws(t, f, *a, **kw):
    """
    throws(t, f, *a, **kw) -> bool
    Return whether f(*a, **kw) raise exception t.
    
    >>> from throws import throws
    >>> throws(ValueError, int, "xyz")
    True
    >>> throws(ValueError, int, "1")
    False
    """
    
    #doctestのためだけの関数です
    
    try:
        f(*a, **kw)
    except t:
        return True
    else:
        return False

class MultiDict(DictMixin):
    def __init__(self, items=()):
        self._dict = {}
        for k, v in items:
            self[k] = v
    
    def __getitem__(self, key):
        return tuple(self._dict[key])
    
    def __setitem__(self, key, value):
        self._dict.setdefault(key, []).append(value)
    
    def_deligators(locals(), "_dict", 
        "__delitem__ keys __contains__ __iter__ iteritems __copy__".split())
    
    def addList(self, key, values):
        for v in values:
            self[key] = v
    
    def updateList(self, **kw):
        for k, v in kw.iteritems():
            self.addList(k, list(v))
    
    def remove(self, key, val):
        """
        removes the specified value from the list of values for the specified key;
        raises KeyError if key not found,
        raises ValueError if val not found.
        """
        self._dict[key].remove(val)
    
    def get1(self, *a):
        """\
        D.pop1(key[,default]) -> value, 
        
        If there is exactly one value corresponding specified key, 
        return the value.
        
        If two or more values is corresponded for specified key,
        ValueError is raised.
        
        If key is not found, default is returned if given, 
        otherwise KeyError is raised.
        
        >>> from multidict import MultiDict, throws
        >>> m = MultiDict([(0, "a"), (0, "b"), (2, "c"), (1, "d")])
        >>> m.get1(1)
        'd'
        >>> throws(KeyError, m.get1, 3)
        True
        >>> m.get1(3, "e")
        'e'
        >>> throws(ValueError, m.get1, 0)
        True
        >>> throws(ValueError, m.get1, 0, 1)
        True
        """
        if len(a) == 0:
            raise TypeError("get1 expected at least 1 arguments, got 0")
        if len(a) == 2:
            key, default = a
            has_default = True
        elif len(a) == 1:
            key = a[0]
            default = None
            has_default = False
        else:
            msg = "get1 expected at least 1 arguments, got %d"%(len(a),)
            raise TypeError(msg)
        
        try:
            values = self[key]
        except KeyError:
            if has_default:
                return default
            else:
                raise
        else:
            if len(values) == 1:
                return values[0]
            else:
                msg = "There are multiple values for key %r"%(key,)
                raise ValueError(msg)
    
    def pop1(self, *a):
        """\
        D.pop1(key[,default]) -> value, 
        
        If there is exactly one value corresponding specified key, 
        remove the key and return the value.
        
        If two or more values is corresponded for specified key,
        ValueError is raised.
        
        If key is not found, default is returned if given, 
        otherwise KeyError is raised.
        
        >>> from multidict import MultiDict, throws
        >>> m = MultiDict([(0, "a"), (0, "b"), (2, "c"), (1, "d")])
        >>> m.pop1(1)
        'd'
        >>> m
        {0: ['a', 'b'], 2: ['c']}
        >>> throws(KeyError, m.pop1, 1)
        True
        >>> m.pop1(1, "e")
        'e'
        >>> throws(ValueError, m.pop1, 0, "e")
        True
        """
        v = self.get1(*a)
        self.pop(a[0], None)
        return v
    
    def asdict1(self, choice=None):
        d = {}
        for key, values in self.iteritems():
            if len(values) == 1:
                d[key] = values[0]
            else:
                if choice is None:
                    raise KeyError("There are multiple values for key %r"%(key,))
                else:
                    d[key] = choice(self, key, values)
        return d
    
    def iteritems_m(self):
        return ((key, value) 
                    for key, values in self.iteritems()
                        for value in values)
    
    def iterkeys_m(self):
        from operator import itemgetter
        return itertools.imap(itemgetter(0), self.iteritems_m())
    
    def itervalues_m(self):
        from operator import itemgetter
        return itertools.imap(itemgetter(1), self.iteritems_m())
    
    @classmethod
    def choice_last(cls, md, key, values):
        return values[-1]
    
    @classmethod
    def choice_first(cls, md, key, values):
        return values[0]
    
    def items_m(self):
        return list(self.iteritems_m())
    
    def values_m(self):
        return list(self.itervalues_m())
    
    def keys_m(self):
        return list(self.iterkeys_m())
    
if __name__ == "__main__":
    import doctest
    doctest.testmod()