Issue #11297: Add collections.ChainMap()

This commit is contained in:
Raymond Hettinger 2011-02-26 01:02:51 +00:00
parent 692f038a5d
commit 9fe1ccfb5a
6 changed files with 123 additions and 7 deletions

View File

@ -23,6 +23,7 @@ Python's general purpose built-in containers, :class:`dict`, :class:`list`,
===================== ====================================================================
:func:`namedtuple` factory function for creating tuple subclasses with named fields
:class:`deque` list-like container with fast appends and pops on either end
:class:`ChainMap` dict-like class for creating a single view of multiple mappings
:class:`Counter` dict subclass for counting hashable objects
:class:`OrderedDict` dict subclass that remembers the order entries were added
:class:`defaultdict` dict subclass that calls a factory function to supply missing values
@ -37,6 +38,119 @@ Python's general purpose built-in containers, :class:`dict`, :class:`list`,
as well.
:class:`ChainMap` objects
-------------------------
A :class:`ChainMap` class is provided for quickly linking a number of mappings
so they can be treated as a single unit. It is often much faster than creating
a new dictionary and running multiple :meth:`~dict.update` calls.
The class can be used to simulate nested scopes and is useful in templating.
.. class:: ChainMap(*maps)
A :class:`ChainMap` groups multiple dicts or other mappings together to
create a single, updateable view. If no *maps* are specified, a single empty
dictionary is provided so that a new chain always has at least one mapping.
The underlying mappings are stored in a list. That list is public and can
accessed or updated using the *maps* attribute. There is no other state.
Lookups search the underlying mappings successively until a key is found. In
contrast, writes, updates, and deletions only operate on the first mapping.
A class:`ChainMap` incorporates the underlying mappings by reference. So, if
one of the underlying mappings gets updated, those changes will be reflected
in class:`ChainMap`.
All of the usual dictionary methods are supported. In addition, there is a
*maps* attribute, a method for creating new subcontexts, and a property for
accessing all but the first mapping:
.. attribute:: maps
A user updateable list of mappings. The list is ordered from
first-searched to last-searched. It is the only stored state and can
modified to change which mappings are searched. The list should
always contain at least one mapping.
.. method:: new_child()
Returns a new :class:`ChainMap` containing a new :class:`dict` followed by
all of the maps in the current instance. A call to ``d.new_child()`` is
equivalent to: ``ChainMap({}, *d.maps)``. This method is used for
creating subcontexts that can be updated without altering values in any
of the parent mappings.
.. attribute:: parents()
Returns a new :class:`ChainMap` containing all of the maps in the current
instance except the first one. This is useful for skipping the first map
in the search. The use-cases are similar to those for the
:keyword:`nonlocal` keyword used in :term:`nested scopes <nested scope>`.
The use-cases also parallel those for the builtin :func:`super` function.
A reference to ``d.parents`` is equivalent to: ``ChainMap(*d.maps[1:])``.
.. versionadded:: 3.3
Example of simulating Python's internal lookup chain::
import __builtin__
pylookup = ChainMap(locals(), globals(), vars(__builtin__))
Example of letting user specified values take precedence over environment
variables which in turn take precedence over default values::
import os, argparse
defaults = {'color': 'red', 'user': guest}
parser = argparse.ArgumentParser()
parser.add_argument('-u', '--user')
parser.add_argument('-c', '--color')
user_specified = vars(parser.parse_args())
combined = ChainMap(user_specified, os.environ, defaults)
Example patterns for using the :class:`ChainMap` class to simulate nested
contexts::
c = ChainMap() Create root context
d = c.new_child() Create nested child context
e = c.new_child() Child of c, independent from d
e.maps[0] Current context dictionary -- like Python's locals()
e.maps[-1] Root context -- like Python's globals()
e.parents Enclosing context chain -- like Python's nonlocals
d['x'] Get first key in the chain of contexts
d['x'] = 1 Set value in current context
del['x'] Delete from current context
list(d) All nested values
k in d Check all nested values
len(d) Number of nested values
d.items() All nested items
dict(d) Flatten into a regular dictionary
.. seealso::
* The `MultiContext class
<http://svn.enthought.com/svn/enthought/CodeTools/trunk/enthought/contexts/multi_context.py>`_
in the Enthought `CodeTools package
<https://github.com/enthought/codetools>`_\ has options to support
writing to any mapping in the chain.
* Django's `Context class
<http://code.djangoproject.com/browser/django/trunk/django/template/context.py>`_
for templating is a read-only chain of mappings. It also features
pushing and popping of contexts similar to the
:meth:`~collections.ChainMap.new_child` method and the
:meth:`~collections.ChainMap.parents` property.
* The `Nested Contexts recipe
<http://code.activestate.com/recipes/577434/>`_ has options to control
whether writes and other mutations apply only to the first mapping or to
any mapping in the chain.
* A `greatly simplified read-only version of Chainmap
<http://code.activestate.com/recipes/305268/>`_\.
:class:`Counter` objects
------------------------

View File

@ -636,7 +636,7 @@ class Counter(dict):
### ChainMap (helper for configparser and string.Template)
########################################################################
class _ChainMap(MutableMapping):
class ChainMap(MutableMapping):
''' A ChainMap groups multiple dicts (or other mappings) together
to create a single, updateable view.

View File

@ -120,7 +120,7 @@ ConfigParser -- responsible for parsing a list of
"""
from collections.abc import MutableMapping
from collections import OrderedDict as _default_dict, _ChainMap
from collections import OrderedDict as _default_dict, ChainMap as _ChainMap
import functools
import io
import itertools

View File

@ -46,7 +46,7 @@ def capwords(s, sep=None):
####################################################################
import re as _re
from collections import _ChainMap
from collections import ChainMap
class _TemplateMetaclass(type):
pattern = r"""
@ -100,7 +100,7 @@ class Template(metaclass=_TemplateMetaclass):
if not args:
mapping = kws
elif kws:
mapping = _ChainMap(kws, args[0])
mapping = ChainMap(kws, args[0])
else:
mapping = args[0]
# Helper function for .sub()
@ -126,7 +126,7 @@ class Template(metaclass=_TemplateMetaclass):
if not args:
mapping = kws
elif kws:
mapping = _ChainMap(kws, args[0])
mapping = ChainMap(kws, args[0])
else:
mapping = args[0]
# Helper function for .sub()

View File

@ -11,7 +11,7 @@ import keyword
import re
import sys
from collections import UserDict
from collections import _ChainMap as ChainMap
from collections import ChainMap
from collections.abc import Hashable, Iterable, Iterator
from collections.abc import Sized, Container, Callable
from collections.abc import Set, MutableSet
@ -21,7 +21,7 @@ from collections.abc import ByteString
################################################################################
### _ChainMap (helper class for configparser and the string module)
### ChainMap (helper class for configparser and the string module)
################################################################################
class TestChainMap(unittest.TestCase):

View File

@ -35,6 +35,8 @@ Core and Builtins
Library
-------
- Issue #11297: Add collections.ChainMap().
- Issue #10755: Add the posix.fdlistdir() function. Patch by Ross Lagerwall.
- Issue #4761: Add the *at() family of functions (openat(), etc.) to the posix