ChainMap — Búsqueda en varios diccionarios

La clase ChainMap maneja una secuencia de diccionarios, y busca a través de ellos en el orden en que se les da para encontrar valores asociados con llaves. Un ChainMap hace un buen contenedor de «contexto», ya que se puede tratar como una pila para la cual ocurren cambios a medida que la pila crece, con estos cambios siendo descartados nuevamente a medida que la pila se reduce.

Accediendo a valores

El ChainMap admite la misma interfaz que un diccionario normal para acceder a los valores existentes

collections_chainmap_read.py
import collections

a = {'a': 'A', 'c': 'C'}
b = {'b': 'B', 'c': 'D'}

m = collections.ChainMap(a, b)

print('Individual Values')
print('a = {}'.format(m['a']))
print('b = {}'.format(m['b']))
print('c = {}'.format(m['c']))
print()

print('Keys = {}'.format(list(m.keys())))
print('Values = {}'.format(list(m.values())))
print()

print('Items:')
for k, v in m.items():
    print('{} = {}'.format(k, v))
print()

print('"d" in m: {}'.format(('d' in m)))

Las asignaciones de elementos secundarios se buscan en el orden en que se pasan al constructor, por lo que el valor informado para la clave 'c' proviene del diccionario a.

$ python3 collections_chainmap_read.py

Individual Values
a = A
b = B
c = C

Keys = ['c', 'b', 'a']
Values = ['C', 'B', 'A']

Items:
c = C
b = B
a = A

"d" in m: False

Reordenando

El ChainMap almacena la lista de asignaciones sobre las cuales busca en una lista en su atributo maps. Esta lista es mutable, por lo que es posible agregar nuevas asignaciones directamente o cambiar el orden de los elementos para controlar el comportamiento de búsqueda y actualización.

collections_chainmap_reorder.py
import collections

a = {'a': 'A', 'c': 'C'}
b = {'b': 'B', 'c': 'D'}

m = collections.ChainMap(a, b)

print(m.maps)
print('c = {}\n'.format(m['c']))

# reverse the list
m.maps = list(reversed(m.maps))

print(m.maps)
print('c = {}'.format(m['c']))

Cuando se invierte la lista de asignaciones, el valor asociado con 'c' cambia.

$ python3 collections_chainmap_reorder.py

[{'c': 'C', 'a': 'A'}, {'c': 'D', 'b': 'B'}]
c = C

[{'c': 'D', 'b': 'B'}, {'c': 'C', 'a': 'A'}]
c = D

Actualizando valores

Un ChainMap no almacena en caché los valores en las asignaciones de elementos secundarios. Por lo tanto, si se modifican sus contenidos, los resultados se reflejan cuando el ChainMap es accedido.

collections_chainmap_update_behind.py
import collections

a = {'a': 'A', 'c': 'C'}
b = {'b': 'B', 'c': 'D'}

m = collections.ChainMap(a, b)
print('Before: {}'.format(m['c']))
a['c'] = 'E'
print('After : {}'.format(m['c']))

Cambiar los valores asociados con las claves existentes y agregar nuevos elementos funciona de la misma manera.

$ python3 collections_chainmap_update_behind.py

Before: C
After : E

También es posible establecer valores a través del ChainMap directamente, aunque solo el primer mapeo en la cadena es en realidad modificado.

collections_chainmap_update_directly.py
import collections

a = {'a': 'A', 'c': 'C'}
b = {'b': 'B', 'c': 'D'}

m = collections.ChainMap(a, b)
print('Before:', m)
m['c'] = 'E'
print('After :', m)
print('a:', a)

Cuando el nuevo valor se almacena usando m, el mapeo a es actualizado.

$ python3 collections_chainmap_update_directly.py

Before: ChainMap({'c': 'C', 'a': 'A'}, {'c': 'D', 'b': 'B'})
After : ChainMap({'c': 'E', 'a': 'A'}, {'c': 'D', 'b': 'B'})
a: {'c': 'E', 'a': 'A'}

ChainMap proporciona un método conveniente para crear una nueva instancia con un mapeo adicional al frente de la lista maps para que sea fácil evitar la modificación de las estructuras de datos subyacentes existentes.

collections_chainmap_new_child.py
import collections

a = {'a': 'A', 'c': 'C'}
b = {'b': 'B', 'c': 'D'}

m1 = collections.ChainMap(a, b)
m2 = m1.new_child()

print('m1 before:', m1)
print('m2 before:', m2)

m2['c'] = 'E'

print('m1 after:', m1)
print('m2 after:', m2)

Este comportamiento de apilamiento es lo que hace que sea conveniente usar instancias ChainMap como contextos de plantilla o aplicación. Específicamente, es fácil de agregar o actualizar valores en una iteración, luego descartar los cambios para la próxima iteración.

$ python3 collections_chainmap_new_child.py

m1 before: ChainMap({'c': 'C', 'a': 'A'}, {'c': 'D', 'b': 'B'})
m2 before: ChainMap({}, {'c': 'C', 'a': 'A'}, {'c': 'D', 'b':
'B'})
m1 after: ChainMap({'c': 'C', 'a': 'A'}, {'c': 'D', 'b': 'B'})
m2 after: ChainMap({'c': 'E'}, {'c': 'C', 'a': 'A'}, {'c': 'D',
'b': 'B'})

Para situaciones donde el nuevo contexto es conocido o construido de antemano, también es posible pasar un mapeo a new_child().

collections_chainmap_new_child_explicit.py
import collections

a = {'a': 'A', 'c': 'C'}
b = {'b': 'B', 'c': 'D'}
c = {'c': 'E'}

m1 = collections.ChainMap(a, b)
m2 = m1.new_child(c)

print('m1["c"] = {}'.format(m1['c']))
print('m2["c"] = {}'.format(m2['c']))

Este es el equivalente de

m2 = collections.ChainMap(c, *m1.maps)

y produce

$ python3 collections_chainmap_new_child_explicit.py

m1["c"] = C
m2["c"] = E