Commit fa11db0a authored by Raymond Hettinger's avatar Raymond Hettinger

Issue #9825: Replace OrderedDict.__del__() with weakrefs.

parent 9be0b2e3
...@@ -11,12 +11,16 @@ from operator import itemgetter as _itemgetter ...@@ -11,12 +11,16 @@ from operator import itemgetter as _itemgetter
from keyword import iskeyword as _iskeyword from keyword import iskeyword as _iskeyword
import sys as _sys import sys as _sys
import heapq as _heapq import heapq as _heapq
from weakref import proxy as _proxy
from itertools import repeat as _repeat, chain as _chain, starmap as _starmap from itertools import repeat as _repeat, chain as _chain, starmap as _starmap
################################################################################ ################################################################################
### OrderedDict ### OrderedDict
################################################################################ ################################################################################
class _Link(object):
__slots__ = 'prev', 'next', 'key', '__weakref__'
class OrderedDict(dict, MutableMapping): class OrderedDict(dict, MutableMapping):
'Dictionary that remembers insertion order' 'Dictionary that remembers insertion order'
# An inherited dict maps keys to values. # An inherited dict maps keys to values.
...@@ -27,7 +31,7 @@ class OrderedDict(dict, MutableMapping): ...@@ -27,7 +31,7 @@ class OrderedDict(dict, MutableMapping):
# The internal self.__map dictionary maps keys to links in a doubly linked list. # 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 circular doubly linked list starts and ends with a sentinel element.
# The sentinel element never gets deleted (this simplifies the algorithm). # The sentinel element never gets deleted (this simplifies the algorithm).
# Each link is stored as a list of length three: [PREV, NEXT, KEY]. # The back links are weakref proxies (to prevent circular references).
def __init__(self, *args, **kwds): def __init__(self, *args, **kwds):
'''Initialize an ordered dictionary. Signature is the same as for '''Initialize an ordered dictionary. Signature is the same as for
...@@ -40,51 +44,53 @@ class OrderedDict(dict, MutableMapping): ...@@ -40,51 +44,53 @@ class OrderedDict(dict, MutableMapping):
try: try:
self.__root self.__root
except AttributeError: except AttributeError:
self.__root = root = [None, None, None] # sentinel node self.__root = root = _Link() # sentinel node for the doubly linked list
PREV = 0 root.prev = root.next = root
NEXT = 1
root[PREV] = root[NEXT] = root
self.__map = {} self.__map = {}
self.update(*args, **kwds) self.update(*args, **kwds)
def __setitem__(self, key, value, PREV=0, NEXT=1, dict_setitem=dict.__setitem__): def __setitem__(self, key, value,
dict_setitem=dict.__setitem__, proxy=_proxy, Link=_Link):
'od.__setitem__(i, y) <==> od[i]=y' 'od.__setitem__(i, y) <==> od[i]=y'
# Setting a new item creates a new link which goes at the end of the linked # 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. # list, and the inherited dictionary is updated with the new key/value pair.
if key not in self: if key not in self:
self.__map[key] = link = Link()
root = self.__root root = self.__root
last = root[PREV] last = root.prev
last[NEXT] = root[PREV] = self.__map[key] = [last, root, key] link.prev, link.next, link.key = last, root, key
dict_setitem(self, key, value) last.next = link
root.prev = proxy(link)
dict.__setitem__(self, key, value)
def __delitem__(self, key, PREV=0, NEXT=1, dict_delitem=dict.__delitem__): def __delitem__(self, key, dict_delitem=dict.__delitem__):
'od.__delitem__(y) <==> del od[y]' 'od.__delitem__(y) <==> del od[y]'
# Deleting an existing item uses self.__map to find the link which is # 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. # then removed by updating the links in the predecessor and successor nodes.
dict_delitem(self, key) dict_delitem(self, key)
link = self.__map.pop(key) link = self.__map.pop(key)
link_prev = link[PREV] link_prev = link.prev
link_next = link[NEXT] link_next = link.next
link_prev[NEXT] = link_next link_prev.next = link_next
link_next[PREV] = link_prev link_next.prev = link_prev
def __iter__(self, NEXT=1, KEY=2): def __iter__(self):
'od.__iter__() <==> iter(od)' 'od.__iter__() <==> iter(od)'
# Traverse the linked list in order. # Traverse the linked list in order.
root = self.__root root = self.__root
curr = root[NEXT] curr = root.next
while curr is not root: while curr is not root:
yield curr[KEY] yield curr.key
curr = curr[NEXT] curr = curr.next
def __reversed__(self, PREV=0, KEY=2): def __reversed__(self):
'od.__reversed__() <==> reversed(od)' 'od.__reversed__() <==> reversed(od)'
# Traverse the linked list in reverse order. # Traverse the linked list in reverse order.
root = self.__root root = self.__root
curr = root[PREV] curr = root.prev
while curr is not root: while curr is not root:
yield curr[KEY] yield curr.key
curr = curr[PREV] curr = curr.prev
def __reduce__(self): def __reduce__(self):
'Return state information for pickling' 'Return state information for pickling'
...@@ -99,16 +105,12 @@ class OrderedDict(dict, MutableMapping): ...@@ -99,16 +105,12 @@ class OrderedDict(dict, MutableMapping):
def clear(self): def clear(self):
'od.clear() -> None. Remove all items from od.' 'od.clear() -> None. Remove all items from od.'
try: root = self.__root
for node in self.__map.values(): root.prev = root.next = root
del node[:] self.__map.clear()
self.__root[:] = [self.__root, self.__root, None]
self.__map.clear()
except AttributeError:
pass
dict.clear(self) dict.clear(self)
def popitem(self, last=True, PREV=0, NEXT=1, KEY=2, dict_pop=dict.pop): def popitem(self, last=True):
'''od.popitem() -> (k, v), return and remove a (key, value) pair. '''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. Pairs are returned in LIFO order if last is true or FIFO order if false.
...@@ -116,21 +118,45 @@ class OrderedDict(dict, MutableMapping): ...@@ -116,21 +118,45 @@ class OrderedDict(dict, MutableMapping):
if not self: if not self:
raise KeyError('dictionary is empty') raise KeyError('dictionary is empty')
root = self.__root root = self.__root
if last: # link_prev <--> link <--> root if last:
link = root[PREV] link = root.prev
link_prev = link[PREV] link_prev = link.prev
link_prev[NEXT] = root link_prev.next = root
root[PREV] = link_prev root.prev = link_prev
else: # root <--> link <--> link_next else:
link = root[NEXT] link = root.next
link_next = link[NEXT] link_next = link.next
root[NEXT] = link_next root.next = link_next
link_next[PREV] = root link_next.prev = root
key = link[KEY] key = link.key
del self.__map[key] del self.__map[key]
value = dict_pop(self, key) value = dict.pop(self, key)
return key, value return key, value
def move_to_end(self, key, last=True):
'''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).
'''
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
setdefault = MutableMapping.setdefault setdefault = MutableMapping.setdefault
update = MutableMapping.update update = MutableMapping.update
pop = MutableMapping.pop pop = MutableMapping.pop
...@@ -170,33 +196,6 @@ class OrderedDict(dict, MutableMapping): ...@@ -170,33 +196,6 @@ class OrderedDict(dict, MutableMapping):
all(p==q for p, q in zip(self.items(), other.items())) all(p==q for p, q in zip(self.items(), other.items()))
return dict.__eq__(self, other) return dict.__eq__(self, other)
def __del__(self):
self.clear() # eliminate cyclical references
def move_to_end(self, key, last=True, PREV=0, NEXT=1):
'''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).
'''
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
################################################################################ ################################################################################
### namedtuple ### namedtuple
......
...@@ -32,6 +32,12 @@ Core and Builtins ...@@ -32,6 +32,12 @@ Core and Builtins
Library Library
------- -------
- Issue #9825: Using __del__ in the definition of collections.OrderedDict made
it possible for the user to create self-referencing ordered dictionaries
which become permanently uncollectable GC garbage. Reinstated the Py3.1
approach of using weakref proxies so that reference cycles never get created
in the first place.
- Issue #9579, #9580: Fix os.confstr() for value longer than 255 bytes and - Issue #9579, #9580: Fix os.confstr() for value longer than 255 bytes and
encode the value with filesystem encoding and surrogateescape (instead of encode the value with filesystem encoding and surrogateescape (instead of
utf-8 in strict mode). Patch written by David Watson. utf-8 in strict mode). Patch written by David Watson.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment