Python递归setattr() - 类似于处理嵌套字典的函数

时间:2013-07-31 20:37:47

标签: python algorithm recursion nested setattr

有许多好的getattr()函数用于解析嵌套字典结构,例如:

我想制作一个并行的setattr()。基本上,给定:

cmd = 'f[0].a'
val = 'whatever'
x = {"a":"stuff"}

我想制作一个我可以分配的功能:

x['f'][0]['a'] = val

或多或少,这与以下方式相同:

setattr(x,'f[0].a',val)

产量:

>>> x
{"a":"stuff","f":[{"a":"whatever"}]}

我现在称之为setByDot()

setByDot(x,'f[0].a',val)

这样做的一个问题是,如果中间的密钥不存在,则需要检查并创建一个中间密钥(如果它不存在) - 即,对于上述情况:

>>> x = {"a":"stuff"}
>>> x['f'][0]['a'] = val
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'f'

所以,你首先必须做出:

>>> x['f']=[{}]
>>> x
{'a': 'stuff', 'f': [{}]}
>>> x['f'][0]['a']=val
>>> x
{'a': 'stuff', 'f': [{'a': 'whatever'}]}

另一个是当下一个项目是一个字符串时键入的时间与下一个项目是字符串时的键控不同,即:

>>> x = {"a":"stuff"}
>>> x['f']=['']
>>> x['f'][0]['a']=val
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'str' object does not support item assignment

...失败,因为赋值是空字符串而不是空字典。对于dict中的每个非列表,null dict将是正确的赋值,直到最后一个 - 可能是列表或值。

@TokenMacGuy在下面的评论中指出的第二个问题是,当你必须创建一个不存在的列表时,你可能需要创建大量的空白值。所以,

setattr(x,'f[10].a',val)

---可能意味着算法必须制作一个像:

这样的中间体
>>> x['f']=[{},{},{},{},{},{},{},{},{},{},{}]
>>> x['f'][10]['a']=val

产生

>>> x 
{"a":"stuff","f":[{},{},{},{},{},{},{},{},{},{},{"a":"whatever"}]}

这样就是与getter相关联的setter ......

>>> getByDot(x,"f[10].a")
"whatever"

更重要的是,中间体应该/不/覆盖已经存在的值。

以下是我到目前为止的一个简单的想法 - 我可以识别列表与dicts和其他数据类型,并在它们不存在的地方创建它们。但是,我没有看到(a)在哪里进行递归调用,或者(b)如何在迭代列表时“构建”深层对象,以及(c)如何区分/探测/我是当我从/ setting /构造深对象时,当我到达堆栈的末尾时我必须这样做。

def setByDot(obj,ref,newval):
    ref = ref.replace("[",".[")
    cmd = ref.split('.')
    numkeys = len(cmd)
    count = 0
    for c in cmd:
        count = count+1
        while count < numkeys:
            if c.find("["):
                idstart = c.find("[")
                numend = c.find("]")
                try:
                    deep = obj[int(idstart+1:numend-1)]
                except:
                    obj[int(idstart+1:numend-1)] = []
                    deep = obj[int(idstart+1:numend-1)]
            else:
                try:
                    deep = obj[c]
                except:
                    if obj[c] isinstance(dict):
                        obj[c] = {}
                    else:
                        obj[c] = ''
                    deep = obj[c]
        setByDot(deep,c,newval)

这看起来非常棘手,因为如果你正在制作占位符,你必须提前检查/ next / object的类型,并且你必须在后面建立一条路径

更新

我最近也回答了this question,这可能是相关或有帮助的。

4 个答案:

答案 0 :(得分:2)

你可以通过解决两个问题来解决问题:

  1. 访问越界时自动增长的列表(PaddedList)
  2. 一种延迟决定创建内容(dict列表)的方法,直到你第一次访问它为止(DictOrList)
  3. 所以代码看起来像这样:

    import collections
    
    class PaddedList(list):
        """ List that grows automatically up to the max index ever passed"""
        def __init__(self, padding):
            self.padding = padding
    
        def __getitem__(self, key):
            if  isinstance(key, int) and len(self) <= key:
                self.extend(self.padding() for i in xrange(key + 1 - len(self)))
            return super(PaddedList, self).__getitem__(key)
    
    class DictOrList(object):
        """ Object proxy that delays the decision of being a List or Dict """
        def __init__(self, parent):
            self.parent = parent
    
        def __getitem__(self, key):
            # Type of the structure depends on the type of the key
            if isinstance(key, int):
                obj = PaddedList(MyDict)
            else:
                obj = MyDict()
    
            # Update parent references with the selected object
            parent_seq = (self.parent if isinstance(self.parent, dict)
                          else xrange(len(self.parent)))
            for i in parent_seq:
                if self == parent_seq[i]:
                    parent_seq[i] = obj
                    break
    
            return obj[key]
    
    
    class MyDict(collections.defaultdict):
        def __missing__(self, key):
            ret = self[key] = DictOrList(self)
            return ret
    
    def pprint_mydict(d):
        """ Helper to print MyDict as dicts """
        print d.__str__().replace('defaultdict(None, {', '{').replace('})', '}')
    
    x = MyDict()
    x['f'][0]['a'] = 'whatever'
    
    y = MyDict()
    y['f'][10]['a'] = 'whatever'
    
    pprint_mydict(x)
    pprint_mydict(y)
    

    x和y的输出将是:

    {'f': [{'a': 'whatever'}]}
    {'f': [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {'a': 'whatever'}]}
    

    诀窍在于创建一个对象的defaultdict,它可以是一个dict或一个列表,具体取决于你如何访问它。 因此,当您拥有分配x['f'][10]['a'] = 'whatever'时,它将按以下方式工作:

    1. 获取X [&#39; f&#39;]。它不会存在,因此它将返回索引的DictOrList对象&#39; f&#39;
    2. 获取X [&#39; f&#39;] [10]。将使用整数索引调用DictOrList。 getitem 。 DictOrList对象将通过PaddedList
    3. 替换父集合中的自身
    4. 访问PaddedList中的第11个元素将使其增长11个元素,并将返回该位置的MyDict元素
    5. 分配&#34;无论什么&#34;到x [&#39; f&#39;] [10] [&#39; a&#39;]
    6. PaddedList和DictOrList都有点hacky,但是在完成任务之后没有更多的魔法,你有一个dicts和list的结构。

答案 1 :(得分:2)

我将此分为两步。在第一步中,查询字符串被分解为一系列指令。这样问题就解耦了,我们可以在运行它们之前查看指令,也不需要递归调用。

def build_instructions(obj, q):
    """
    Breaks down a query string into a series of actionable instructions.

    Each instruction is a (_type, arg) tuple.
    arg -- The key used for the __getitem__ or __setitem__ call on
           the current object.
    _type -- Used to determine the data type for the value of
             obj.__getitem__(arg)

    If a key/index is missing, _type is used to initialize an empty value.
    In this way _type provides the ability to
    """
    arg = []
    _type = None
    instructions = []
    for i, ch in enumerate(q):
        if ch == "[":
            # Begin list query
            if _type is not None:
                arg = "".join(arg)
                if _type == list and arg.isalpha():
                    _type = dict
                instructions.append((_type, arg))
                _type, arg = None, []
            _type = list
        elif ch == ".":
            # Begin dict query
            if _type is not None:
                arg = "".join(arg)
                if _type == list and arg.isalpha():
                    _type = dict
                instructions.append((_type, arg))
                _type, arg = None, []

            _type = dict
        elif ch.isalnum():
            if i == 0:
                # Query begins with alphanum, assume dict access
                _type = type(obj)

            # Fill out args
            arg.append(ch)
        else:
            TypeError("Unrecognized character: {}".format(ch))

    if _type is not None:
        # Finish up last query
        instructions.append((_type, "".join(arg)))

    return instructions

为您的例子

>>> x = {"a": "stuff"}
>>> print(build_instructions(x, "f[0].a"))
[(<type 'dict'>, 'f'), (<type 'list'>, '0'), (<type 'dict'>, 'a')]

预期的返回值只是指令中下一个元组的_type(第一项)。这非常重要,因为它允许我们正确地初始化/重建丢失的密钥。

这意味着我们的第一条指令在dict上运行,设置或获取密钥'f',并且预计会返回list。同样,我们的第二条指令在list上运行,设置或获取索引0,并且预计会返回dict

现在让我们创建我们的_setattr函数。这将获得正确的指令并完成它们,根据需要创建键值对。最后,它还设置了我们提供的val

def _setattr(obj, query, val):
    """
    This is a special setattr function that will take in a string query,
    interpret it, add the appropriate data structure to obj, and set val.

    We only define two actions that are available in our query string:
    .x -- dict.__setitem__(x, ...)
    [x] -- list.__setitem__(x, ...) OR dict.__setitem__(x, ...)
           the calling context determines how this is interpreted.
    """
    instructions = build_instructions(obj, query)
    for i, (_, arg) in enumerate(instructions[:-1]):
        _type = instructions[i + 1][0]
        obj = _set(obj, _type, arg)

    _type, arg = instructions[-1]
    _set(obj, _type, arg, val)

def _set(obj, _type, arg, val=None):
    """
    Helper function for calling obj.__setitem__(arg, val or _type()).
    """
    if val is not None:
        # Time to set our value
        _type = type(val)

    if isinstance(obj, dict):
        if arg not in obj:
            # If key isn't in obj, initialize it with _type()
            # or set it with val
            obj[arg] = (_type() if val is None else val)
        obj = obj[arg]
    elif isinstance(obj, list):
        n = len(obj)
        arg = int(arg)
        if n > arg:
            obj[arg] = (_type() if val is None else val)
        else:
            # Need to amplify our list, initialize empty values with _type()
            obj.extend([_type() for x in range(arg - n + 1)])
        obj = obj[arg]
    return obj

只是因为我们可以,这是一个_getattr函数。

def _getattr(obj, query):
    """
    Very similar to _setattr. Instead of setting attributes they will be
    returned. As expected, an error will be raised if a __getitem__ call
    fails.
    """
    instructions = build_instructions(obj, query)
    for i, (_, arg) in enumerate(instructions[:-1]):
        _type = instructions[i + 1][0]
        obj = _get(obj, _type, arg)

    _type, arg = instructions[-1]
    return _get(obj, _type, arg)


def _get(obj, _type, arg):
    """
    Helper function for calling obj.__getitem__(arg).
    """
    if isinstance(obj, dict):
        obj = obj[arg]
    elif isinstance(obj, list):
        arg = int(arg)
        obj = obj[arg]
    return obj

行动中:

>>> x = {"a": "stuff"}
>>> _setattr(x, "f[0].a", "test")
>>> print x
{'a': 'stuff', 'f': [{'a': 'test'}]}
>>> print _getattr(x, "f[0].a")
"test"

>>> x = ["one", "two"]
>>> _setattr(x, "3[0].a", "test")
>>> print x
['one', 'two', [], [{'a': 'test'}]]
>>> print _getattr(x, "3[0].a")
"test"

现在有些很酷的东西。与python不同,我们的_setattr函数可以设置不可用的dict键。

x = []
_setattr(x, "1.4", "asdf")
print x
[{}, {'4': 'asdf'}]  # A list, which isn't hashable

>>> y = {"a": "stuff"}
>>> _setattr(y, "f[1.4]", "test")  # We're indexing f with 1.4, which is a list!
>>> print y
{'a': 'stuff', 'f': [{}, {'4': 'test'}]}
>>> print _getattr(y, "f[1.4]")  # Works for _getattr too
"test"

我们不是真的使用不可用的dict键,但看起来我们使用的是查询语言,所以谁在乎呢,对吧!

最后,您可以在同一个对象上运行多个_setattr调用,只需自己尝试一下。

答案 2 :(得分:1)

>>> class D(dict):
...     def __missing__(self, k):
...         ret = self[k] = D()
...         return ret
... 
>>> x=D()
>>> x['f'][0]['a'] = 'whatever'
>>> x
{'f': {0: {'a': 'whatever'}}}

答案 3 :(得分:1)

可以通过覆盖__getitem__来合成递归设置项/属性,以返回可以在原始函数中设置值的代理返回。

我碰巧正在处理一个类似于此的事情的库,所以我正在研究一个可以在实例化时动态分配自己的子类的类。它使得处理这类事情变得更容易,但是如果这种黑客行为让你感到娇气,你可以通过创建类似于我创建的ProxyObject并通过在函数中动态创建ProxyObject使用的各个类来获得类似的行为。 。像

这样的东西
class ProxyObject(object):
    ... #see below

def instanciateProxyObjcet(val):
   class ProxyClassForVal(ProxyObject,val.__class__):
       pass
   return ProxyClassForVal(val)

您可以使用我在下面的FlexibleObject中使用的字典,如果这是您实现它的方式,那么该实现将显着提高效率。我将提供的代码使用FlexibleObject。现在它只支持类,几乎所有Python的内置类都可以通过将自己的实例作为其__init__ / __new__的唯一参数来生成。在接下来的一两周内,我将添加对pickleable的支持,并链接到包含它的github存储库。这是代码:

class FlexibleObject(object):
    """ A FlexibleObject is a baseclass for allowing type to be declared
        at instantiation rather than in the declaration of the class.

        Usage:
        class DoubleAppender(FlexibleObject):
            def append(self,x):
                super(self.__class__,self).append(x)
                super(self.__class__,self).append(x)

        instance1 = DoubleAppender(list)
        instance2 = DoubleAppender(bytearray)
    """
    classes = {}
    def __new__(cls,supercls,*args,**kws):
        if isinstance(supercls,type):
            supercls = (supercls,)
        else:
            supercls = tuple(supercls)
        if (cls,supercls) in FlexibleObject.classes:
            return FlexibleObject.classes[(cls,supercls)](*args,**kws)
        superclsnames = tuple([c.__name__ for c in supercls])
        name = '%s%s' % (cls.__name__,superclsnames)
        d = dict(cls.__dict__)
        d['__class__'] = cls
        if cls == FlexibleObject:
            d.pop('__new__')
        try:
            d.pop('__weakref__')
        except:
            pass
        d['__dict__'] = {}
        newcls = type(name,supercls,d)
        FlexibleObject.classes[(cls,supercls)] = newcls
        return newcls(*args,**kws)

然后使用它来合成查找属性和类字典对象的项目,你可以这样做:

class ProxyObject(FlexibleObject):
    @classmethod
    def new(cls,obj,quickrecdict,path,attribute_marker):
        self = ProxyObject(obj.__class__,obj)
        self.__dict__['reference'] = quickrecdict
        self.__dict__['path'] = path
        self.__dict__['attr_mark'] = attribute_marker
        return self
    def __getitem__(self,item):
        path = self.__dict__['path'] + [item]
        ref = self.__dict__['reference']
        return ref[tuple(path)]
    def __setitem__(self,item,val):
        path = self.__dict__['path'] + [item]
        ref = self.__dict__['reference']
        ref.dict[tuple(path)] = ProxyObject.new(val,ref,
                path,self.__dict__['attr_mark'])
    def __getattribute__(self,attr):
        if attr == '__dict__':
            return object.__getattribute__(self,'__dict__')
        path = self.__dict__['path'] + [self.__dict__['attr_mark'],attr]
        ref = self.__dict__['reference']
        return ref[tuple(path)]
    def __setattr__(self,attr,val):
        path = self.__dict__['path'] + [self.__dict__['attr_mark'],attr]
        ref = self.__dict__['reference']
        ref.dict[tuple(path)] = ProxyObject.new(val,ref,
                path,self.__dict__['attr_mark'])

class UniqueValue(object):
    pass

class QuickRecursiveDict(object):
    def __init__(self,dictionary={}):
        self.dict = dictionary
        self.internal_id = UniqueValue()
        self.attr_marker = UniqueValue()
    def __getitem__(self,item):
        if item in self.dict:
            val = self.dict[item]
            try:
                if val.__dict__['path'][0] == self.internal_id:
                    return val
                else:
                    raise TypeError
            except:
                return ProxyObject.new(val,self,[self.internal_id,item],
                        self.attr_marker)
        try:
            if item[0] == self.internal_id:
                return ProxyObject.new(KeyError(),self,list(item),
                        self.attr_marker)
        except TypeError:
            pass #Item isn't iterable
        return ProxyObject.new(KeyError(),self,[self.internal_id,item],
                    self.attr_marker)
    def __setitem__(self,item,val):
        self.dict[item] = val

实施细节将根据您的需要而有所不同。在代理中覆盖__getitem__显然比覆盖__getitem____getattribute____getattr__要容易得多。您在setbydot中使用的语法使得看起来您最满意的是一些解决方案会覆盖两者的混合。

如果您只是使用字典来比较值,请使用=,&lt; =,&gt; =等。覆盖__getattribute__非常有效。如果你想做一些更复杂的事情,你可能最好覆盖__getattr__并在__setattr__做一些检查以确定你是否想通过在字典中设置一个值来合成设置属性或者您是否想要实际设置您获得的项目的属性。或者您可能想要处理它,以便如果您的对象具有属性,__getattribute__将返回该属性的代理,__setattr__始终只是在对象中设置属性(在这种情况下,您可以完全省略它)。所有这些都取决于你想要使用字典的确切内容。

您也可能想要创建__iter__等。制作它们需要花费一些精力,但细节应遵循__getitem____setitem__的实施。

最后,我将简要总结一下QuickRecursiveDict的行为,以防检查时不能立即清楚。 try / excepts只是检查是否可以执行if的简写。合成递归设置而不是找到一种方法的一个主要缺点是,当您尝试访问尚未设置的密钥时,您不能再提高KeyErrors。但是,通过返回KeyError的子类,我可以非常接近,这是我在示例中所做的。我没有测试它,所以我不会将它添加到代码中,但您可能希望将一些人类可读的键表示传递给KeyError。

但除此之外,它的效果相当不错。

>>> qrd = QuickRecursiveDict
>>> qrd[0][13] # returns an instance of a subclass of KeyError
>>> qrd[0][13] = 9
>>> qrd[0][13] # 9
>>> qrd[0][13]['forever'] = 'young'
>>> qrd[0][13] # 9
>>> qrd[0][13]['forever'] # 'young'
>>> qrd[0] # returns an instance of a subclass of KeyError
>>> qrd[0] = 0
>>> qrd[0] # 0
>>> qrd[0][13]['forever'] # 'young'

还有一点需要注意,返回的东西并不像它看起来那样。它是它的外观的代理。如果您需要int 9,则需要int(qrd[0][13])而不是qrd[0][13]。对于ints,这并不重要,因为,+, - ,=和所有绕过__getattribute__但是对于列表,如果你没有重新制作它们,你将丢失append之类的属性。 (您将保留len和其他内置方法,而不是list的属性。您将失去__len__。)

就是这样。代码有点令人费解,如果您有任何疑问,请告诉我。我可能无法回答他们直到今晚,除非答案非常简短。我希望我能早点看到这个问题,这是一个非常酷的问题,我会尽快更新清洁解决方案。我很高兴尝试在晚上的凌晨编写解决方案。 :)