【问题标题】:How to add an element to the beginning of an OrderedDict?如何将元素添加到 OrderedDict 的开头?
【发布时间】:2013-05-15 22:01:05
【问题描述】:

我有这个:

d1 = OrderedDict([('a', '1'), ('b', '2')])

如果我这样做:

d1.update({'c':'3'})

然后我明白了:

OrderedDict([('a', '1'), ('b', '2'), ('c', '3')])

但我想要这个:

[('c', '3'), ('a', '1'), ('b', '2')]

不创建新字典。

【问题讨论】:

标签: python python-3.x dictionary python-2.x ordereddict


【解决方案1】:

在 Python 2 中没有用于执行此操作的内置方法。如果需要,您需要编写一个 prepend() 方法/函数,该方法/函数在 OrderedDict 内部进行操作,复杂度为 O(1)。

对于 Python 3.2 及更高版本,您应该使用move_to_end 方法。该方法接受一个last 参数,该参数指示元素将被移动到OrderedDict 的底部(last=True)还是顶部(last=False)。

最后,如果您想要一个快速、肮脏和缓慢的解决方案,您可以从头开始创建一个新的OrderedDict

四种不同解决方案的详细信息:


扩展OrderedDict并添加一个新的实例方法

from collections import OrderedDict

class MyOrderedDict(OrderedDict):

    def prepend(self, key, value, dict_setitem=dict.__setitem__):

        root = self._OrderedDict__root
        first = root[1]

        if key in self:
            link = self._OrderedDict__map[key]
            link_prev, link_next, _ = link
            link_prev[1] = link_next
            link_next[0] = link_prev
            link[0] = root
            link[1] = first
            root[1] = first[0] = link
        else:
            root[1] = first[0] = self._OrderedDict__map[key] = [root, first, key]
            dict_setitem(self, key, value)

演示:

>>> d = MyOrderedDict([('a', '1'), ('b', '2')])
>>> d
MyOrderedDict([('a', '1'), ('b', '2')])
>>> d.prepend('c', 100)
>>> d
MyOrderedDict([('c', 100), ('a', '1'), ('b', '2')])
>>> d.prepend('a', d['a'])
>>> d
MyOrderedDict([('a', '1'), ('c', 100), ('b', '2')])
>>> d.prepend('d', 200)
>>> d
MyOrderedDict([('d', 200), ('a', '1'), ('c', 100), ('b', '2')])

操作OrderedDict对象的独立函数

这个函数通过接受dict对象、键和值来做同样的事情。我个人更喜欢这门课:

from collections import OrderedDict

def ordered_dict_prepend(dct, key, value, dict_setitem=dict.__setitem__):
    root = dct._OrderedDict__root
    first = root[1]

    if key in dct:
        link = dct._OrderedDict__map[key]
        link_prev, link_next, _ = link
        link_prev[1] = link_next
        link_next[0] = link_prev
        link[0] = root
        link[1] = first
        root[1] = first[0] = link
    else:
        root[1] = first[0] = dct._OrderedDict__map[key] = [root, first, key]
        dict_setitem(dct, key, value)

演示:

>>> d = OrderedDict([('a', '1'), ('b', '2')])
>>> ordered_dict_prepend(d, 'c', 100)
>>> d
OrderedDict([('c', 100), ('a', '1'), ('b', '2')])
>>> ordered_dict_prepend(d, 'a', d['a'])
>>> d
OrderedDict([('a', '1'), ('c', 100), ('b', '2')])
>>> ordered_dict_prepend(d, 'd', 500)
>>> d
OrderedDict([('d', 500), ('a', '1'), ('c', 100), ('b', '2')])

使用OrderedDict.move_to_end() (Python >= 3.2)

Python 3.2 introduced OrderedDict.move_to_end() 方法。使用它,我们可以在 O(1) 时间内将现有键移动到字典的任一端。

>>> d1 = OrderedDict([('a', '1'), ('b', '2')])
>>> d1.update({'c':'3'})
>>> d1.move_to_end('c', last=False)
>>> d1
OrderedDict([('c', '3'), ('a', '1'), ('b', '2')])

如果我们需要插入一个元素并将其移动到顶部,只需一步,我们可以直接使用它来创建一个prepend() 包装器(此处不提供)。


创建一个新的OrderedDict - 慢!!!

如果您不想这样做并且性能不是问题,那么最简单的方法是创建一个新的字典:

from itertools import chain, ifilterfalse
from collections import OrderedDict


def unique_everseen(iterable, key=None):
    "List unique elements, preserving order. Remember all elements ever seen."
    # unique_everseen('AAAABBBCCDAABBB') --> A B C D
    # unique_everseen('ABBCcAD', str.lower) --> A B C D
    seen = set()
    seen_add = seen.add
    if key is None:
        for element in ifilterfalse(seen.__contains__, iterable):
            seen_add(element)
            yield element
    else:
        for element in iterable:
            k = key(element)
            if k not in seen:
                seen_add(k)
                yield element

d1 = OrderedDict([('a', '1'), ('b', '2'),('c', 4)])
d2 = OrderedDict([('c', 3), ('e', 5)])   #dict containing items to be added at the front
new_dic = OrderedDict((k, d2.get(k, d1.get(k))) for k in \
                                           unique_everseen(chain(d2, d1)))
print new_dic

输出:

OrderedDict([('c', 3), ('e', 5), ('a', '1'), ('b', '2')])

【讨论】:

  • 注意如果c已经存在,这不会更新旧值
  • @IARI 如果您指的是move_to_end,那么问题上没有 Python 3 标签,move_to_end 仅适用于 Python 3.2+。我将更新我的答案以包含基于 Python 3 的解决方案。非常感谢您的更新!
  • @AshwiniChaudhary 既然 Python 3 已经添加了move_to_front,那么实现move_to_front 方法而不是单独的prepend 方法可能更好?如果您需要从同一个代码库同时支持 Python 2 和 Python 3,这将使您的代码更具可移植性。
  • dict_setitem=dict.__setitem__ 作为prepend 的参数背后的原因是什么?为什么会/应该通过不同的二传手?
  • 上面的ordered_dict_prepend中一定有bug。调用ordered_dict_prepend(d, 'c', 100) 两次并尝试打印结果字典(只需在Python 的控制台中输入d)会导致Python 进程不断占用内存。使用 Python 2.7.10 测试
【解决方案2】:

如果您需要不存在的功能,只需使用您想要的任何内容扩展类即可:

from collections import OrderedDict

class OrderedDictWithPrepend(OrderedDict):
    def prepend(self, other):
        ins = []
        if hasattr(other, 'viewitems'):
            other = other.viewitems()
        for key, val in other:
            if key in self:
                self[key] = val
            else:
                ins.append((key, val))
        if ins:
            items = self.items()
            self.clear()
            self.update(ins)
            self.update(items)

效率不高,但有效:

o = OrderedDictWithPrepend()

o['a'] = 1
o['b'] = 2
print o
# OrderedDictWithPrepend([('a', 1), ('b', 2)])

o.prepend({'c': 3})
print o
# OrderedDictWithPrepend([('c', 3), ('a', 1), ('b', 2)])

o.prepend([('a',11),('d',55),('e',66)])
print o
# OrderedDictWithPrepend([('d', 55), ('e', 66), ('c', 3), ('a', 11), ('b', 2)])

【讨论】:

    【解决方案3】:

    我建议向这个纯 Python ActiveState recipe 添加一个 prepend() 方法或从中派生一个子类。考虑到用于排序的底层数据结构是链表,这样做的代码可能相当有效。

    更新

    为了证明这种方法是可行的,下面是执行建议的代码。作为奖励,我还做了一些额外的小改动,以便在 Python 2.7.15 和 3.7.1 中工作。

    prepend() 方法已添加到配方中的类中,并已根据已添加的另一个名为 move_to_end() 的方法实现,该方法已添加到 Python 3.2 中的 OrderedDict

    prepend() 也可以直接实现,几乎与@Ashwini Chaudhary 的answer 开头所示的完全一样——这样做可能会稍微快一些,但这留给有动力的读者作为练习...

    # Ordered Dictionary for Py2.4 from https://code.activestate.com/recipes/576693
    
    # Backport of OrderedDict() class that runs on Python 2.4, 2.5, 2.6, 2.7 and pypy.
    # Passes Python2.7's test suite and incorporates all the latest updates.
    
    try:
        from thread import get_ident as _get_ident
    except ImportError:  # Python 3
    #    from dummy_thread import get_ident as _get_ident
        from _thread import get_ident as _get_ident  # Changed - martineau
    
    try:
        from _abcoll import KeysView, ValuesView, ItemsView
    except ImportError:
        pass
    
    class MyOrderedDict(dict):
        'Dictionary that remembers insertion order'
        # An inherited dict maps keys to values.
        # The inherited dict provides __getitem__, __len__, __contains__, and get.
        # The remaining methods are order-aware.
        # Big-O running times for all methods are the same as for regular dictionaries.
    
        # The internal self.__map dictionary maps keys to links in a doubly linked list.
        # The circular doubly linked list starts and ends with a sentinel element.
        # The sentinel element never gets deleted (this simplifies the algorithm).
        # Each link is stored as a list of length three:  [PREV, NEXT, KEY].
    
        def __init__(self, *args, **kwds):
            '''Initialize an ordered dictionary.  Signature is the same as for
            regular dictionaries, but keyword arguments are not recommended
            because their insertion order is arbitrary.
    
            '''
            if len(args) > 1:
                raise TypeError('expected at most 1 arguments, got %d' % len(args))
            try:
                self.__root
            except AttributeError:
                self.__root = root = []  # sentinel node
                root[:] = [root, root, None]
                self.__map = {}
            self.__update(*args, **kwds)
    
        def prepend(self, key, value):  # Added to recipe.
            self.update({key: value})
            self.move_to_end(key, last=False)
    
        #### Derived from cpython 3.2 source code.
        def move_to_end(self, key, last=True):  # Added to recipe.
            '''Move an existing element to the end (or beginning if last==False).
    
            Raises KeyError if the element does not exist.
            When last=True, acts like a fast version of self[key]=self.pop(key).
            '''
            PREV, NEXT, KEY = 0, 1, 2
    
            link = self.__map[key]
            link_prev = link[PREV]
            link_next = link[NEXT]
            link_prev[NEXT] = link_next
            link_next[PREV] = link_prev
            root = self.__root
    
            if last:
                last = root[PREV]
                link[PREV] = last
                link[NEXT] = root
                last[NEXT] = root[PREV] = link
            else:
                first = root[NEXT]
                link[PREV] = root
                link[NEXT] = first
                root[NEXT] = first[PREV] = link
        ####
    
        def __setitem__(self, key, value, dict_setitem=dict.__setitem__):
            'od.__setitem__(i, y) <==> od[i]=y'
            # Setting a new item creates a new link which goes at the end of the linked
            # list, and the inherited dictionary is updated with the new key/value pair.
            if key not in self:
                root = self.__root
                last = root[0]
                last[1] = root[0] = self.__map[key] = [last, root, key]
            dict_setitem(self, key, value)
    
        def __delitem__(self, key, dict_delitem=dict.__delitem__):
            'od.__delitem__(y) <==> del od[y]'
            # Deleting an existing item uses self.__map to find the link which is
            # then removed by updating the links in the predecessor and successor nodes.
            dict_delitem(self, key)
            link_prev, link_next, key = self.__map.pop(key)
            link_prev[1] = link_next
            link_next[0] = link_prev
    
        def __iter__(self):
            'od.__iter__() <==> iter(od)'
            root = self.__root
            curr = root[1]
            while curr is not root:
                yield curr[2]
                curr = curr[1]
    
        def __reversed__(self):
            'od.__reversed__() <==> reversed(od)'
            root = self.__root
            curr = root[0]
            while curr is not root:
                yield curr[2]
                curr = curr[0]
    
        def clear(self):
            'od.clear() -> None.  Remove all items from od.'
            try:
                for node in self.__map.itervalues():
                    del node[:]
                root = self.__root
                root[:] = [root, root, None]
                self.__map.clear()
            except AttributeError:
                pass
            dict.clear(self)
    
        def popitem(self, last=True):
            '''od.popitem() -> (k, v), return and remove a (key, value) pair.
            Pairs are returned in LIFO order if last is true or FIFO order if false.
    
            '''
            if not self:
                raise KeyError('dictionary is empty')
            root = self.__root
            if last:
                link = root[0]
                link_prev = link[0]
                link_prev[1] = root
                root[0] = link_prev
            else:
                link = root[1]
                link_next = link[1]
                root[1] = link_next
                link_next[0] = root
            key = link[2]
            del self.__map[key]
            value = dict.pop(self, key)
            return key, value
    
        # -- the following methods do not depend on the internal structure --
    
        def keys(self):
            'od.keys() -> list of keys in od'
            return list(self)
    
        def values(self):
            'od.values() -> list of values in od'
            return [self[key] for key in self]
    
        def items(self):
            'od.items() -> list of (key, value) pairs in od'
            return [(key, self[key]) for key in self]
    
        def iterkeys(self):
            'od.iterkeys() -> an iterator over the keys in od'
            return iter(self)
    
        def itervalues(self):
            'od.itervalues -> an iterator over the values in od'
            for k in self:
                yield self[k]
    
        def iteritems(self):
            'od.iteritems -> an iterator over the (key, value) items in od'
            for k in self:
                yield (k, self[k])
    
        def update(*args, **kwds):
            '''od.update(E, **F) -> None.  Update od from dict/iterable E and F.
    
            If E is a dict instance, does:           for k in E: od[k] = E[k]
            If E has a .keys() method, does:         for k in E.keys(): od[k] = E[k]
            Or if E is an iterable of items, does:   for k, v in E: od[k] = v
            In either case, this is followed by:     for k, v in F.items(): od[k] = v
    
            '''
            if len(args) > 2:
                raise TypeError('update() takes at most 2 positional '
                                'arguments (%d given)' % (len(args),))
            elif not args:
                raise TypeError('update() takes at least 1 argument (0 given)')
            self = args[0]
            # Make progressively weaker assumptions about "other"
            other = ()
            if len(args) == 2:
                other = args[1]
            if isinstance(other, dict):
                for key in other:
                    self[key] = other[key]
            elif hasattr(other, 'keys'):
                for key in other.keys():
                    self[key] = other[key]
            else:
                for key, value in other:
                    self[key] = value
            for key, value in kwds.items():
                self[key] = value
    
        __update = update  # let subclasses override update without breaking __init__
    
        __marker = object()
    
        def pop(self, key, default=__marker):
            '''od.pop(k[,d]) -> v, remove specified key and return the corresponding value.
            If key is not found, d is returned if given, otherwise KeyError is raised.
    
            '''
            if key in self:
                result = self[key]
                del self[key]
                return result
            if default is self.__marker:
                raise KeyError(key)
            return default
    
        def setdefault(self, key, default=None):
            'od.setdefault(k[,d]) -> od.get(k,d), also set od[k]=d if k not in od'
            if key in self:
                return self[key]
            self[key] = default
            return default
    
        def __repr__(self, _repr_running={}):
            'od.__repr__() <==> repr(od)'
            call_key = id(self), _get_ident()
            if call_key in _repr_running:
                return '...'
            _repr_running[call_key] = 1
            try:
                if not self:
                    return '%s()' % (self.__class__.__name__,)
                return '%s(%r)' % (self.__class__.__name__, self.items())
            finally:
                del _repr_running[call_key]
    
        def __reduce__(self):
            'Return state information for pickling'
            items = [[k, self[k]] for k in self]
            inst_dict = vars(self).copy()
            for k in vars(MyOrderedDict()):
                inst_dict.pop(k, None)
            if inst_dict:
                return (self.__class__, (items,), inst_dict)
            return self.__class__, (items,)
    
        def copy(self):
            'od.copy() -> a shallow copy of od'
            return self.__class__(self)
    
        @classmethod
        def fromkeys(cls, iterable, value=None):
            '''OD.fromkeys(S[, v]) -> New ordered dictionary with keys from S
            and values equal to v (which defaults to None).
    
            '''
            d = cls()
            for key in iterable:
                d[key] = value
            return d
    
        def __eq__(self, other):
            '''od.__eq__(y) <==> od==y.  Comparison to another OD is order-sensitive
            while comparison to a regular mapping is order-insensitive.
    
            '''
            if isinstance(other, MyOrderedDict):
                return len(self)==len(other) and self.items() == other.items()
            return dict.__eq__(self, other)
    
        def __ne__(self, other):
            return not self == other
    
        # -- the following methods are only used in Python 2.7 --
    
        def viewkeys(self):
            "od.viewkeys() -> a set-like object providing a view on od's keys"
            return KeysView(self)
    
        def viewvalues(self):
            "od.viewvalues() -> an object providing a view on od's values"
            return ValuesView(self)
    
        def viewitems(self):
            "od.viewitems() -> a set-like object providing a view on od's items"
            return ItemsView(self)
    
    if __name__ == '__main__':
    
        d1 = MyOrderedDict([('a', '1'), ('b', '2')])
        d1.update({'c':'3'})
        print(d1)  # -> MyOrderedDict([('a', '1'), ('b', '2'), ('c', '3')])
    
        d2 = MyOrderedDict([('a', '1'), ('b', '2')])
        d2.prepend('c', 100)
        print(d2)  # -> MyOrderedDict([('c', 100), ('a', '1'), ('b', '2')])
    

    【讨论】:

    【解决方案4】:

    您必须创建OrderedDict 的新实例。如果您的密钥是唯一的:

    d1=OrderedDict([("a",1),("b",2)])
    d2=OrderedDict([("c",3),("d",99)])
    both=OrderedDict(list(d2.items()) + list(d1.items()))
    print(both)
    
    #OrderedDict([('c', 3), ('d', 99), ('a', 1), ('b', 2)])
    

    但如果不是,请注意,您可能不希望这种行为:

    d1=OrderedDict([("a",1),("b",2)])
    d2=OrderedDict([("c",3),("b",99)])
    both=OrderedDict(list(d2.items()) + list(d1.items()))
    print(both)
    
    #OrderedDict([('c', 3), ('b', 2), ('a', 1)])
    

    【讨论】:

    • 在python3中,items方法不再返回一个列表,而是一个视图,它的作用就像一个集合。在这种情况下,您需要使用集合并集,因为与 + 连接不起作用: dict(x.items() | y.items())
    • @TheDemz 我认为 set union 不会保留顺序,从而使生成的 OrderedDict 中的项目的最终顺序不稳定?
    • @max 是的,它不稳定。从 dict.keys()、dict.values() 和 dict.items() 返回的对象称为字典视图。它们是惰性序列,可以看到底层字典的变化。要强制字典视图成为完整列表,请使用 list(dictview)。请参阅字典视图对象。 docs.python.org/3.4/library/…
    【解决方案5】:

    编辑(2019-02-03) 请注意,以下答案仅适用于旧版本的 Python。最近,OrderedDict 已用 C 重写。此外,这确实触及了令人不悦的双下划线属性。

    为了类似的目的,我刚刚在我的一个项目中编写了OrderedDict 的一个子类。 Here's the gist.

    与大多数这些解决方案不同,插入操作也是常数时间O(1)(它们不需要您重建数据结构)。

    >>> d1 = ListDict([('a', '1'), ('b', '2')])
    >>> d1.insert_before('a', ('c', 3))
    >>> d1
    ListDict([('c', 3), ('a', '1'), ('b', '2')])
    

    【讨论】:

    • 我在 Python 3.4 上使用它时得到TypeError: '_Link' object does not support indexing
    • 它也不适用于 python 3.5: AttributeError: 'ListDict' object has no attribute '_OrderedDict__map'
    • 这不再有效,因为从 Python 3.5 开始,OrderedDict 一直是 rewritten in C,并且这个子类犯了与内部人员混为一谈的禁忌(实际上是颠倒名称修改以访问 __ 属性)。跨度>
    【解决方案6】:

    现在可以使用 move_to_end(key, last=True)

    >>> d = OrderedDict.fromkeys('abcde')
    >>> d.move_to_end('b')
    >>> ''.join(d.keys())
    'acdeb'
    >>> d.move_to_end('b', last=False)
    >>> ''.join(d.keys())
    'bacde'
    

    https://docs.python.org/3/library/collections.html#collections.OrderedDict.move_to_end

    【讨论】:

      【解决方案7】:

      如果您知道需要一个“c”键,但不知道值,请在创建 dict 时插入带有虚拟值的“c”。

      d1 = OrderedDict([('c', None), ('a', '1'), ('b', '2')])
      

      稍后再更改值。

      d1['c'] = 3
      

      【讨论】:

      • 有没有办法插入一个有序的字典,这样元素仍然是排序的(增加/减少)。
      • 有序字典不按项目的任何属性排序。它是按插入顺序排列的,与物品本身无关。在 CPthon 3.6 和 Python 3.7 中,所有 dicts 都是如此有序的,几乎没有理由使用 OrderedDict。
      【解决方案8】:

      FWIW 这是我为插入任意索引位置而编写的快速脏代码。不一定有效,但它可以就地工作。

      class OrderedDictInsert(OrderedDict):
          def insert(self, index, key, value):
              self[key] = value
              for ii, k in enumerate(list(self.keys())):
                  if ii >= index and k != key:
                      self.move_to_end(k)
      

      【讨论】:

        【解决方案9】:

        您可能希望完全使用不同的结构,但在 python 2.7 中有一些方法。

        d1 = OrderedDict([('a', '1'), ('b', '2')])
        d2 = OrderedDict(c='3')
        d2.update(d1)
        

        d2 将包含

        >>> d2
        OrderedDict([('c', '3'), ('a', '1'), ('b', '2')])
        

        正如其他人所说,在 python 3.2 中,您可以使用OrderedDict.move_to_end('c', last=False) 在插入后移动给定的键。

        注意:请注意,由于创建新的 OrderedDict 和复制旧值,第一个选项对于大型数据集的速度较慢。

        【讨论】:

          【解决方案10】:

          我在尝试使用@Ashwini Chaudhary 打印或保存字典时遇到了一个无限循环,使用 Python 2.7 回答。但我设法减少了他的代码,让它在这里工作:

          def move_to_dict_beginning(dictionary, key):
              """
                  Move a OrderedDict item to its beginning, or add it to its beginning.
                  Compatible with Python 2.7
              """
          
              if sys.version_info[0] < 3:
                  value = dictionary[key]
                  del dictionary[key]
                  root = dictionary._OrderedDict__root
          
                  first = root[1]
                  root[1] = first[0] = dictionary._OrderedDict__map[key] = [root, first, key]
                  dict.__setitem__(dictionary, key, value)
          
              else:
                  dictionary.move_to_end( key, last=False )
          

          【讨论】:

          • 这太棒了!为我工作!
          【解决方案11】:

          这是一个默认的有序字典,允许在任何位置插入项目并使用 .运算符创建键:

          from collections import OrderedDict
          
          class defdict(OrderedDict):
          
              _protected = ["_OrderedDict__root", "_OrderedDict__map", "_cb"]    
              _cb = None
          
              def __init__(self, cb=None):
                  super(defdict, self).__init__()
                  self._cb = cb
          
              def __setattr__(self, name, value):
                  # if the attr is not in self._protected set a key
                  if name in self._protected:
                      OrderedDict.__setattr__(self, name, value)
                  else:
                      OrderedDict.__setitem__(self, name, value)
          
              def __getattr__(self, name):
                  if name in self._protected:
                      return OrderedDict.__getattr__(self, name)
                  else:
                      # implements missing keys
                      # if there is a callable _cb, create a key with its value
                      try:
                          return OrderedDict.__getitem__(self, name)
                      except KeyError as e:
                          if callable(self._cb):
                              value = self[name] = self._cb()
                              return value
                          raise e
          
              def insert(self, index, name, value):
                  items = [(k, v) for k, v in self.items()]
                  items.insert(index, (name, value))
                  self.clear()
                  for k, v in items:
                      self[k] = v
          
          
          asd = defdict(lambda: 10)
          asd.k1 = "Hey"
          asd.k3 = "Bye"
          asd.k4 = "Hello"
          asd.insert(1, "k2", "New item")
          print asd.k5 # access a missing key will create one when there is a callback
          # 10
          asd.k6 += 5  # adding to a missing key
          print asd.k6
          # 15
          print asd.keys()
          # ['k1', 'k2', 'k3', 'k4', 'k5', 'k6']
          print asd.values()
          # ['Hey', 'New item', 'Bye', 'Hello', 10, 15]
          

          【讨论】:

            猜你喜欢
            • 2021-04-16
            • 2020-06-30
            • 2012-10-08
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2020-02-20
            • 1970-01-01
            • 2011-03-20
            相关资源
            最近更新 更多