【问题标题】:Python recursive setattr()-like function for working with nested dictionaries用于处理嵌套字典的 Python 递归 setattr()-like 函数
【发布时间】:2013-08-01 13:50:04
【问题描述】:

有很多很好的类似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

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

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

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)如何区分 /probing/ 我是当我到达堆栈的末尾时,我必须从 /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/ 对象的类型,并且您必须向后看以构建路径.

更新

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

【问题讨论】:

    标签: python algorithm recursion nested setattr


    【解决方案1】:

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

    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 调用,自己试试吧。

    【讨论】:

    • 这看起来很酷,但似乎对我不起作用。 &gt;&gt;&gt; x = {"a": "stuff"} &gt;&gt;&gt; _setattr(x, "f[0].a", "test") &gt;&gt;&gt; x {'a': 'stuff'}
    • @Mittenchops 出于某种原因,我保存的 build_instructions 与 SO 上的不同。我已经更新了它,所以它现在应该可以工作了。
    • @Mittenchops 哈哈,原来我无意中切换了build_instructions 中的参数。如果还有任何问题,请告诉我!
    【解决方案2】:

    可以通过重写 __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 中使用的语法让您看起来对某些覆盖两者的混合解决方案最满意。

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

    您可能还想创建__iter__ 等。制作起来需要一点点努力,但细节应该遵循__getitem____setitem__的实现。

    最后,我将简要总结QuickRecursiveDict 的行为,以防检查后无法立即清楚。 try/excepts 只是检查ifs 是否可以执行的简写。合成递归设置而不是找到一种方法的一个主要缺陷是,当您尝试访问尚未设置的键时,您不能再引发 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]。对于整数,这无关紧要,因为 +,-,= 和所有绕过 __getattribute__ 的东西,但对于列表,如果你不重铸它们,你会失去像 append 这样的属性。 (你会保留len 和其他内置方法,而不是list 的属性。你会失去__len__。)

    就是这样。代码有点复杂,如果您有任何问题,请告诉我。除非答案真的很简短,否则我可能要到今晚才能回答他们。我希望我能早点看到这个问题,这是一个非常酷的问题,我会尽快尝试更新更清洁的解决方案。我在昨晚凌晨尝试编写解决方案时很开心。 :)

    【讨论】:

      【解决方案3】:

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

      1. 越界访问时自动增长的列表 (PaddedList)
      2. 一种延迟决定创建内容(dict 列表)的方法,直到您第一次访问它(DictOrList)

      所以代码将如下所示:

      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'}]}
      

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

      1. 获取 X['f']。它不会存在,因此它将为索引“f”返回一个 DictOrList 对象
      2. 获取 X['f'][10]。将使用整数索引调用 DictOrList.getitem。 DictOrList 对象将在父集合中将其自身替换为 PaddedList
      3. 访问 PaddedList 中的第 11 个元素会将其增加 11 个元素,并将返回该位置的 MyDict 元素
      4. 将“whatever”分配给 x['f'][10]['a']

      PaddedList 和 DictOrList 都有些老套,但是在所有的分配之后没有更多的魔法,你有一个字典和列表的结构。

      【讨论】:

      • 对不起,我不明白——你能告诉我这是如何作为一个二传手工作的,从function(x,'f[10].a',val)x['f'][10]['a'] = val='whatever'
      • 您还可以将填充列表实现为 default_dict,假设索引是整数并且 __iter__ 将返回 itervalues()。
      • @dbw 我考虑过,但更喜欢使用列表,因为不确定如何使用(即切片、排序......)
      • 好点。切片表示法需要扩展列表,而不是制作稀疏列表。
      【解决方案4】:
      >>> 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'}}}
      

      【讨论】:

      • 嗯,我喜欢它 /much/ 更简单,而且很接近,但 x 需要返回 {"f":[{"a":"whatever"}]} 而不是 {'f': {0: {'a': 'whatever'}}} 其中中间是列表的第 0 个,而不是值键为 0。我想我可以使用这个,或者可能是采用 dict-to-object 解析器或其他东西的一般方法......
      • 如果x=D();x['f'][100]['a'] = 'whatevs' 是列表而不是字典,应该怎么做?
      • 是的,好点,但我想它需要为项目 0 到 99 创建虚拟对象。这也意味着如果您按 x['f'][ 的顺序分配它们可能意味着100]['a'] = 'whatevs', x['f'][99]['a'] = 'more',如果你填写一个空字符串,你可以覆盖你为 99 所做的任何值,或者空列表或空字典。
      猜你喜欢
      • 2021-12-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-02-05
      • 2018-09-28
      • 1970-01-01
      相关资源
      最近更新 更多