ChainMap — инструмент управления поиском в словарях и не только

Работая с несколькими словарями, иногда нужно сгруппировать их и управлять ими как единым словарём. В других ситуациях у вас есть словари, представляющие различные области видимости, контексты и, чтобы получить данные в определённом порядке или с определённым приоритетом, нужно работать с ними как с единым словарём.

В обоих случаях можно воспользоваться ChainMap из модуля коллекций. ChainMap группирует словари и отображает их в единое, обновляемое представление с поведением, подобным поведению словаря, а также представляет возможности для эффективного управления различными словарями, определения их ключей, свойств и не только. К старту курса о Fullstack-разработке на Python делимся статьёй о ChainMap, где вы найдёте ссылку на истоки появления класса — баг в Python и, конечно, примеры его применения на практике и в стандартной библиотеке Python.


В этом руководстве вы:

  • Научитесь создавать экземпляр ChainMap в программах на Python.

  • Поймёте разницу между ChainMap и словарём.

  • Поработаете несколькими словарями через ChainMap.

  • Будете управлять приоритетом поиска ключей в ChainMap.

Чтобы руководство принесло больше пользы, вы должны знать основы работы со списками Python. В конце путешествия вы найдёте несколько практических примеров, которые помогут лучше понять самые полезные особенности и варианты применения ChainMap.

Начинаем

ChainMap добавили в коллекции Python 3.3 как инструмент управления контекстами и множествами. ChainMap не объединяет представления: он хранит их во внутреннем списке отображений. Также в нём переопределены общие операции со словарём наверху списка. Поскольку внутренний список содержит ссылки на оригинальное входное отображение, изменения в этих отображениях влияют на весь объект ChainMap.

Хранение входных отображений в списке позволяет иметь дубликаты ключей в данном ChainMap. В поиске ключа ChainMap ищет список отображений, пока не находит первое совпадение с искомым ключом. Если ключа нет, поднимется KeyError.

Хранение отображений по-настоящему полезно в управлении несколькими областями видимости, где каждое отображение представляет отдельный scope или контекст. Чтобы лучше понять происходящее, подумайте, как Python разрешает имена. В поиске имени он ищет в locals(), globals() и встроенных именах до первого вхождения. Если имени не существует, поднимется NameError. Работа с областями видимости и контекстами — самая распространённая задача, которая решается через ChainMap. Работая с этим классом, вы можете в цепочку объединить словари с ключами, которые либо пересекаются (как множества), либо нет.

В первом случае ChainMap позволяет рассматривать словарь как единое целое. Вы можете получить доступ к парам ключ-значение, как если бы вы работали с одним словарём. Во втором случае, помимо управления вашими словарями как одним, вы также можете воспользоваться внутренним списком отображений, чтобы определить некий приоритет доступа для повторяющихся ключей в ваших словарях. Именно поэтому объекты ChainMap подходят в работе с несколькими контекстами.

Любопытно поведение ChainMap: мутации, такие как обновление, добавление, удаление, очистка и перемещение ключей наверх, действуют только на первое отображение внутреннего списка. Вот основные особенности ChainMap, он:

  • Создаёт обновляемое представление из нескольких входных отображений.

  • Предоставляет почти тот же интерфейс, что и словарь, но с дополнительными функциями.

  • Не объединяет входные отображения, сохраняя их во внутреннем открытом списке.

  • Видит внешние изменения во входных отображениях.

  • Может содержать повторяющиеся ключи с разными значениями.

  • Последовательно ищет ключи по внутреннему списку отображений.

  • Поднимает KeyError, когда ключ отсутствует после поиска по всему списку отображений.

  • Выполняет мутации только на первом отображении внутреннего списка.

Создание экземпляра ChainMap

Чтобы создать ChainMap в коде Python, нужно импортировать этот класс из коллекций, а затем вызвать его. Инициализатор класса может принимать в качестве аргументов ноль или более отображений. Без аргументов он инициализирует ChainMap пустым словарём:

>>> from collections import ChainMap
>>> from collections import OrderedDict, defaultdict

>>> # Use no arguments
>>> ChainMap()
ChainMap({})

>>> # Use regular dictionaries
>>> numbers = {"one": 1, "two": 2}
>>> letters = {"a": "A", "b": "B"}

>>> ChainMap(numbers, letters)
ChainMap({'one': 1, 'two': 2}, {'a': 'A', 'b': 'B'})

>>> ChainMap(numbers, {"a": "A", "b": "B"})
ChainMap({'one': 1, 'two': 2}, {'a': 'A', 'b': 'B'})

>>> # Use other mappings
>>> numbers = OrderedDict(one=1, two=2)
>>> letters = defaultdict(str, {"a": "A", "b": "B"})
>>> ChainMap(numbers, letters)
ChainMap(
    OrderedDict([('one', 1), ('two', 2)]),
    defaultdict(<class 'str'>, {'a': 'A', 'b': 'B'})
)

Выше создаются объекты ChainMap с различными комбинациями отображений. В каждом случае ChainMap возвращает одно представление, похожее на словарь, всех входных отображений. Обратите внимание, что вы можете использовать любой тип отображения, например OrderedDict или defaultdict.

Объекты ChainMap возможно создавать через fromkeys(), который может принимать повторяющиеся ключи и необязательное значение по умолчанию для всех ключей:

from collections import ChainMap

>>> ChainMap.fromkeys(["one", "two","three"])
ChainMap({'one': None, 'two': None, 'three': None})

>>> ChainMap.fromkeys(["one", "two","three"], 0)
ChainMap({'one': 0, 'two': 0, 'three': 0})

Если вызвать .fromkeys() на ChainMap с повторяющимся набором ключей как аргументом, вы получаете ChainMap с одним словарём. Ключи берутся из итерируемых входных данных, а значения по умолчанию равны None. При необходимости вы можете передать второй аргумент в .fromkeys(), чтобы предоставить осмысленное значение по умолчанию для каждого ключа.

Операции, подобные операциям словаря

Для доступа к существующим ключам ChainMap поддерживает тот же API, что и обычные словари. Как только у вас появится объект ChainMap, вы сможете получить существующие ключи через их поиск в стиле словаря или использовать get():

>>> from collections import ChainMap

>>> numbers = {"one": 1, "two": 2}
>>> letters = {"a": "A", "b": "B"}
>>> alpha_num = ChainMap(numbers, letters)
>>> alpha_num["two"]
2
>>> alpha_num["three"]
Traceback (most recent call last):
    ...
KeyError: 'three'

Поиск по ключу ищет по всём отображениям по отображению целевой цепочки, пока не найдёт нужный ключ. Если ключа не существует, вы получаете обычную KeyError. А что найдётся, если у ключей есть дубликаты? Первое вхождение целевого ключа:

>>> from collections import ChainMap

>>> for_adoption = {"dogs": 10, "cats": 7, "pythons": 3}
>>> vet_treatment = {"dogs": 4, "cats": 3, "turtles": 1}
>>> pets = ChainMap(for_adoption, vet_treatment)

>>> pets["dogs"]
10
>>> pets.get("cats")
7
>>> pets["turtles"]
1

При доступе к дубликату ключа, например "dogs" и "cats", ChainMap возвращает только первое вхождение. Внутренне операции поиска ищут входные отображения в том же порядке, в каком они отображаются во внутреннем списке отображений, это также точный порядок их передачи вами в инициализатор класса. Такое общее поведение применяется и к итерации:

>>> from collections import ChainMap

>>> for_adoption = {"dogs": 10, "cats": 7, "pythons": 3}
>>> vet_treatment = {"dogs": 4, "cats": 3, "turtles": 1}
>>> pets = ChainMap(for_adoption, vet_treatment)

>>> for key, value in pets.items():
...     print(key, "->", value)
...
dogs -> 10
cats -> 7
turtles -> 1
pythons -> 3

Цикл for выполняет итерацию по словарям домашних животных и выводит первое вхождение каждой пары ключ-значение. Возможно итерировать словарь напрямую или через .keys() и .values(), как с любым словарём:

>>> from collections import ChainMap

>>> for_adoption = {"dogs": 10, "cats": 7, "pythons": 3}
>>> vet_treatment = {"dogs": 4, "cats": 3, "turtles": 1}
>>> pets = ChainMap(for_adoption, vet_treatment)

>>> for key in pets:
...     print(key, "->", pets[key])
...
dogs -> 10
cats -> 7
turtles -> 1
pythons -> 3

>>> for key in pets.keys():
...     print(key, "->", pets[key])
...
dogs -> 10
cats -> 7
turtles -> 1
pythons -> 3

>>> for value in pets.values():
...     print(value)
...
10
7
1
3

Его поведение аналогично. Каждая итерация проходит через первое вхождение каждого ключа, элемента и значения в базовой ChainMap. ChainMap поддерживает мутации, то есть он позволяет обновлять, добавлять, удалять и вставлять пары ключ-значение. Разница здесь в том, что эти операции действуют только на первое отображение:

>>> from collections import ChainMap

>>> numbers = {"one": 1, "two": 2}
>>> letters = {"a": "A", "b": "B"}

>>> alpha_num = ChainMap(numbers, letters)
>>> alpha_num
ChainMap({'one': 1, 'two': 2}, {'a': 'A', 'b': 'B'})

>>> # Add a new key-value pair
>>> alpha_num["c"] = "C"
>>> alpha_num
ChainMap({'one': 1, 'two': 2, 'c': 'C'}, {'a': 'A', 'b': 'B'})

>>> # Update an existing key
>>> alpha_num["b"] = "b"
>>> alpha_num
ChainMap({'one': 1, 'two': 2, 'c': 'C', 'b': 'b'}, {'a': 'A', 'b': 'B'})

>>> # Pop keys
>>> alpha_num.pop("two")
2
>>> alpha_num.pop("a")
Traceback (most recent call last):
    ...
KeyError: "Key not found in the first mapping: 'a'"

>>> # Delete keys
>>> del alpha_num["c"]
>>> alpha_num
ChainMap({'one': 1, 'b': 'b'}, {'a': 'A', 'b': 'B'})
>>> del alpha_num["a"]
Traceback (most recent call last):
    ...
KeyError: "Key not found in the first mapping: 'a'"

>>> # Clear the dictionary
>>> alpha_num.clear()
>>> alpha_num
ChainMap({}, {'a': 'A', 'b': 'B'})

Операции, которые изменяют содержимое данной ChainMap, влияют только на первое отображение, даже если изменяемый ключ существует в других отображениях из списка. Когда вы пытаетесь обновить "b" во втором отображении, на самом деле вы добавляете новый ключ в первый словарь. Вы можете воспользоваться этим поведением для создания обновляемых ChainMap, которые не изменяют оригинальные входные словари. В этом случае в качестве первого аргумента для ChainMap можно воспользоваться пустым словарём:

>>> from collections import ChainMap

>>> numbers = {"one": 1, "two": 2}
>>> letters = {"a": "A", "b": "B"}

>>> alpha_num = ChainMap({}, numbers, letters)
>>> alpha_num
ChainMap({}, {'one': 1, 'two': 2}, {'a': 'A', 'b': 'B'})

>>> alpha_num["comma"] = ","
>>> alpha_num["period"] = "."

>>> alpha_num
ChainMap(
    {'comma': ',', 'period': '.'},
    {'one': 1, 'two': 2},
    {'a': 'A', 'b': 'B'}
)

Выше пустой словарь ({}) создаёт alpha_num. Такой подход гарантирует, что изменения в alpha_num не повлияют на два исходных входных словаря, цифры и буквы, повлияв только на пустой словарь в начале списка.

Сравним объединение и цепочку словарей

Как альтернативу объединению нескольких словарей в цепочку с помощью ChainMap вы можете объединить их функцией dict.update():

>>> from collections import ChainMap

>>> # Chain dictionaries with ChainMap
>>> for_adoption = {"dogs": 10, "cats": 7, "pythons": 3}
>>> vet_treatment = {"hamsters": 2, "turtles": 1}

>>> ChainMap(for_adoption, vet_treatment)
ChainMap(
    {'dogs': 10, 'cats': 7, 'pythons': 3},
    {'hamsters': 2, 'turtles': 1}
)

>>> # Merge dictionaries with .update()
>>> pets = {}
>>> pets.update(for_adoption)
>>> pets.update(vet_treatment)
>>> pets
{'dogs': 10, 'cats': 7, 'pythons': 3, 'hamsters': 2, 'turtles': 1}

Выше вы получаете аналогичные результаты при построении ChainMap и эквивалентного словаря из двух существующих словарей с уникальными ключами.

Объединение словарей с помощью .update() имеет плюсы и минусы по сравнению с объединением в цепочку через ChainMap. Первый и самый важный недостаток — вы отказываетесь от возможности управления доступом к повторяющимся ключам и определения приоритетов с использованием нескольких областей или контекстов. С .update() последнее значение, указанное вами для данного ключа, всегда будет иметь приоритет:

>>> for_adoption = {"dogs": 10, "cats": 7, "pythons": 3}
>>> vet_treatment = {"cats": 2, "dogs": 1}

>>> # Merge dictionaries with .update()
>>> pets = {}
>>> pets.update(for_adoption)
>>> pets.update(vet_treatment)
>>> pets
{'dogs': 1, 'cats': 2, 'pythons': 3}

Обычные словари не хранят повторяющиеся ключи. Каждый раз, когда вы вызываете .update() со значением существующего ключа, этот ключ обновляется, вы теряете возможность определять приоритет доступа к дубликатам ключей с использованием разных областей видимости.

Начиная с Python 3.5, вы также можете объединять словари через оператор распаковки словаря (**), а в Python 3.9 есть оператор объединения словарей (|) для объединения двух словарей в новый.

Предположим, у вас n различных отображений, каждое из которых содержит не более m ключей. Создание объекта ChainMap из них занимает O(n) времени выполнения; получение ключа — O(n) в худшем сценарии, где целевой ключ находится в последнем словаре внутреннего списка отображений. Создание обычного словаря через update() в цикле займёт O(n*m), извлечение ключа из окончательного словаря — O(1).

Вывод: если вы часто создаёте цепочки словарей и каждый раз выполняете только несколько поисков ключей, эффективнее работать с ChainMap. Если всё наоборот, то используйте обычные словари, когда не требуются дубликаты ключей или несколько областей.

Ещё одно различие между объединением и цепочкой словарей заключается в том, что при использовании ChainMap внешние изменения во входных словарях влияют на базовую цепочку, чего не происходит в случае объединённых словарей.

Дополнительные возможности ChainMap

ChainMap предоставляет в основном тот же API и функции, что и обычный словарь Python, с некоторыми тонкими различиями, о которых вы уже знаете, а также поддерживает некоторые дополнительные функции, специфичные для его дизайна и целей.

Управление списком отображений через .maps

ChainMap хранит все входные отображения во внутреннем списке. Этот список доступен через открытый атрибут экземпляра под названием .maps, и он может быть изменён пользователем. Порядок отображений в .maps соответствует порядку их передачи вами в ChainMap. Этот порядок определяет порядок операций поиска ключа. Вот пример применения .maps:

>>> from collections import ChainMap

>>> for_adoption = {"dogs": 10, "cats": 7, "pythons": 3}
>>> vet_treatment = {"dogs": 4, "turtles": 1}

>>> pets = ChainMap(for_adoption, vet_treatment)
>>> pets.maps
[{'dogs': 10, 'cats': 7, 'pythons': 3}, {'dogs': 4, 'turtles': 1}]

Здесь вы используете .maps для доступа к внутреннему списку отображений, хранящихся в домашних животных. Этот список — обычный список Python, поэтому вы можете добавлять и удалять отображения вручную, выполнять итерацию по списку, изменять порядок отображений и делать многое другое:

>>> from collections import ChainMap

>>> for_adoption = {"dogs": 10, "cats": 7, "pythons": 3}
>>> vet_treatment = {"cats": 1}
>>> pets = ChainMap(for_adoption, vet_treatment)

>>> pets.maps.append({"hamsters": 2})
>>> pets.maps
[{'dogs': 10, 'cats': 7, 'pythons': 3}, {"cats": 1}, {'hamsters': 2}]

>>> del pets.maps[1]
>>> pets.maps
[{'dogs': 10, 'cats': 7, 'pythons': 3}, {'hamsters': 2}]

>>> for mapping in pets.maps:
...     print(mapping)
...
{'dogs': 10, 'cats': 7, 'pythons': 3}
{'hamsters': 2}

Выше с помощью .append() сначала добавляется новый словарь в .maps(). Затем вы используете ключевое слово del, чтобы удалить словарь в позиции 1. Вы можете управлять .maps так же, как любым обычным списком Python.

Внутренний список отображений, .maps, всегда будет содержать по крайней мере одно отображение. Например, если вы создадите пустую ChainMap с помощью ChainMap() без аргументов, то в списке будет храниться пустой словарь.

Вы можете использовать .maps для перебора всех отображений во время выполнения действий над ними. Такой перебор позволяет выполнять различные действия с каждым отображением. С помощью этой опции вы можете обойти поведение по умолчанию, которое заключается в изменении только первого отображения в списке.

Интересный пример: с помощью .reverse() возможно изменить порядок текущего списка отображений:

>>> from collections import ChainMap

>>> for_adoption = {"dogs": 10, "cats": 7, "pythons": 3}
>>> vet_treatment = {"cats": 1}
>>> pets = ChainMap(for_adoption, vet_treatment)
>>> pets
ChainMap({'dogs': 10, 'cats': 7, 'pythons': 3}, {'cats': 1})

>>> pets.maps.reverse()
>>> pets
ChainMap({'cats': 1}, {'dogs': 10, 'cats': 7, 'pythons': 3})

Изменение внутреннего списка отображений позволяет изменить порядок при поиске заданного ключа по ChainMap. Теперь, когда вы ищете кошек, то получаете количество кошек на лечении у ветеринара, но не кошек, которых можно забрать.

Добавим подконтекст через .new_child()

ChainMap реализует .new_child(), который опционально принимает отображение как аргумент и возвращает новый экземпляр ChainMap, содержащий входное отображение, за отображением следуют все текущие отображения в базовой ChainMap:

>>> from collections import ChainMap

>>> mom = {"name": "Jane", "age": 31}
>>> dad = {"name": "John", "age": 35}

>>> family = ChainMap(mom, dad)
>>> family
ChainMap({'name': 'Jane', 'age': 31}, {'name': 'John', 'age': 35})

>>> son = {"name": "Mike", "age": 0}
>>> family = family.new_child(son)

>>> for person in family.maps:
...     print(person)
...
{'name': 'Mike', 'age': 0}
{'name': 'Jane', 'age': 31}
{'name': 'John', 'age': 35}

Здесь .new_child() возвращает новый объект ChainMap, содержащий новое отображение — son, за которым следуют старые — mom и dad. Обратите внимание, что новое отображение теперь занимает первую позицию во внутреннем списке отображений, .maps. С помощью .new_child () вы можете создать подконтекст, который можно обновить, не изменяя ни одно отображение. Например, если вы вызываете .new_child() без аргумента, то он использует пустой словарь и помещает его в начало .maps. После этого возможно выполнить любые мутации над новым пустым отображением, оставив остальную его часть доступной только для чтения.

Пропуск подконтекстов с помощью .parents

Ещё одна интересная особенность ChainMap — это свойство .parents, которое возвращает новый экземпляр ChainMap со всеми отображениями в базовой ChainMap, кроме первого, и полезно для пропуска первого отображения при поиске ключей в заданной ChainMap:

>>> from collections import ChainMap

>>> mom = {"name": "Jane", "age": 31}
>>> dad = {"name": "John", "age": 35}
>>> son = {"name": "Mike", "age":  0}

>>> family = ChainMap(son, mom, dad)
>>> family
ChainMap(
    {'name': 'Mike', 'age': 0},
    {'name': 'Jane', 'age': 31},
    {'name': 'John', 'age': 35}
)

>>> family.parents
ChainMap({'name': 'Jane', 'age': 31}, {'name': 'John', 'age': 35})

Выше .parents применяется, чтобы проигнорировать первый словарь с данными son. В каком-то смысле этот метод выполняет действие, обратное .new_child(). Первый метод удаляет словарь, второй добавляет новый словарь в начало списка. В обоих случаях вы получаете новый ChainMap.

Управление областями и контекстами через ChainMap

Наверное, основной вариант применения ChainMap — предоставление эффективного способа управления несколькими областями или контекстами и обработки приоритетов доступа для дубликатов ключей. Эта функция полезна, когда у вас есть словари, которые хранят дублирующиеся ключи, и вы хотите определить порядок обращения кода к этим словарям.

В документации по ChainMap вы найдёте классический пример, эмулирующий то, как Python разрешает имена переменных в различных пространствах имён. В поиске имени интерпретатор последовательно просматривает локальную, глобальную и встроенную области видимости, следуя в том же порядке, пока не найдёт искомое имя. Области видимости Python — это словари, отображающие имена на объекты. Чтобы эмулировать внутреннюю цепочку поиска Python, можно использовать ChainMap:

>>> import builtins

>>> # Shadow input with a global name
>>> input = 42

>>> pylookup = ChainMap(locals(), globals(), vars(builtins))

>>> # Retrieve input from the global namespace
>>> pylookup["input"]
42

>>> # Remove input from the global namespace
>>> del globals()["input"]

>>> # Retrieve input from the builtins namespace
>>> pylookup["input"]
<built-in function input>

Выше вначале создаётся глобальная переменная input, которая затеняет встроенную функцию input() в области видимости builtins. Затем вы создаёте pylookup как ChainMap с тремя словарями, где хранится каждая область видимости Python.

Когда вы получаете ввод из pylookup, вы получаете значение 42 из глобальной области видимости. Если удалить ключ input из словаря globals() и снова обратиться к нему, вы получите встроенную функцию input() из области видимости builtins с самым низким приоритетом в цепочке поиска Python.

Аналогично вы можете использовать ChainMap для определения и управления порядком поиска дубликатов ключей. Это позволяет установить приоритет доступа к нужному экземпляру дублирующего ключа.

ChainMap в стандартной библиотеке

Происхождение ChainMap тесно связано с проблемой производительности в ConfigParser в модуле стандартной библиотеки configparser. В ChainMap разработчики ядра Python значительно улучшили производительность этого модуля в целом, оптимизировав реализацию ConfigParser.get().

ChainMap — часть Template в модуле string. Этот класс как аргумент принимает строковый шаблон и позволяет выполнять подстановку строк, как описывается в PEP 292. Шаблон входной строки содержит встроенные идентификаторы, которые впоследствии можно заменить фактическими значениями:

>>> import string

>>> greeting = "Hey $name, welcome to $place!"
>>> template = string.Template(greeting)

>>> template.substitute({"name": "Jane", "place": "the World"})
'Hey Jane, welcome to the World!'

Когда вы предоставляете значения name и place через словарь, .substitute() заменяет их в строке шаблона. Кроме того, .substitute() может принимать значения как аргументы ключевых слов (**kwargs), что иногда приводит к коллизии имён:

>>> import string

>>> greeting = "Hey $name, welcome to $place!"
>>> template = string.Template(greeting)

>>> template.substitute(
...     {"name": "Jane", "place": "the World"},
...     place="Real Python"
... )
'Hey Jane, welcome to Real Python!'

В этом примере .substitute() заменяет place значением, предоставленное как именованный аргумент, вместо значения во входном словаре. Если вы немного покопаетесь в коде этого метода, то увидите, что он использует ChainMap для эффективного управления приоритетом входных значений, когда происходит коллизия имён. Вот фрагмент реализации .substitute():

# string.py
# Snip...
from collections import ChainMap as _ChainMap

_sentinel_dict = {}

class Template:
    """A string class for supporting $-substitutions."""
    # Snip...

    def substitute(self, mapping=_sentinel_dict, /, **kws):
        if mapping is _sentinel_dict:
            mapping = kws
        elif kws:
            mapping = _ChainMap(kws, mapping)
        # Snip...

В строке mapping = _ChainMap(kws, mapping) происходит магия. Она использует ChainMap, которая как аргументы принимает два словаря, kws и mapping. Помещая kws как первый аргумент, метод устанавливает приоритет для дубликатов идентификаторов во входных данных.

ChainMap в действии

Варианты применения ChainMap довольно специфичны. Это:

  • Эффективная группировка словарей в одном представлении.

  • Поиск по словарям с определённым приоритетом.

  • Предоставление цепочки значений по умолчанию и управление их приоритетом.

  • Повышение производительности кода, часто вычисляющего подмножества словаря.

Практические примеры раздела ниже помогут вам лучше понять, как использовать ChainMap в решении реальных задач.

Доступ к нескольким инвентаризациям как к одной

Ниже ChainMap применяется для эффективного поиска в нескольких словарях в одном представлении. Здесь можно предположить, что есть несколько независимых словарей с уникальными ключами.

Допустим, вы управляете магазином, где продаются фрукты и овощи. Чтобы управлять запасами, вы написали приложение на Python. Оно считывает данные из базы и возвращает два словаря о ценах на фрукты и овощи соответственно. Вам нужен эффективный способ группировки и управления этими данными в едином словаре. Вы решаете воспользоваться ChainMap:

>>> from collections import ChainMap

>>> fruits_prices = {"apple": 0.80, "grape": 0.40, "orange": 0.50}
>>> veggies_prices = {"tomato": 1.20, "pepper": 1.30, "onion": 1.25}
>>> prices = ChainMap(fruits_prices, veggies_prices)

>>> order = {"apple": 4, "tomato": 8, "orange": 4}

>>> for product, units in order.items():
...     price = prices[product]
...     subtotal = units * price
...     print(f"{product:6}: ${price:.2f} × {units} = ${subtotal:.2f}")
...
apple : $0.80 × 4 = $3.20
tomato: $1.20 × 8 = $9.60
orange: $0.50 × 4 = $2.00

Выше вы создаёте подобный словарю объект, группирующий данные из fruits_prices и veggies_prices. Цикл for перебирает продукты в заданном порядке. Затем он рассчитывает итоговую сумму оплаты по каждому виду товара и выводит её на экран.

Можно подумать о том, чтобы сгруппировать данные в новом словаре, через .update() в цикле. Это может отлично сработать, если у вас ограниченный ассортимент продукции и небольшой складской запас.

Однако, если вы управляете множеством продуктов разных типов, .update() по сравнению с ChainMap для создания нового словаря может оказаться неэффективным. ChainMap для решения такого рода задач также может помочь вам определить приоритеты для продуктов из разных партий, что позволит вам управлять запасами по принципу "первым пришёл — первым вышел" (FIFO).

Настройка приоритетов в приложениях командной строки

ChainMap особенно полезна для управления значениями конфигурации по умолчанию. Как вы уже знаете, одна из главных особенностей ChainMap — он позволяет устанавливать приоритеты операций поиска ключей. Звучит как подходящий инструмент для решения проблемы управления конфигурациями в ваших приложениях.

Допустим, вы работаете над приложением для интерфейса командной строки (CLI). Оно позволяет пользователю указать прокси-сервис для подключения к интернету. Тогда приоритеты поиска будут такими

  1. Параметры командной строки (--proxy, -p)

  2. Локальные файлы конфигурации в домашнем каталоге пользователя 

  3. Общесистемная конфигурация прокси

Если пользователь указывает прокси в командной строке, то приложение должно использовать его. В противном случае оно должно использовать системный прокси и так далее. Это один из наиболее распространённых случаев применения ChainMap. Можно сделать следующее:

>>> from collections import ChainMap

>>> cmd_proxy = {}  # The user doesn't provide a proxy
>>> local_proxy = {"proxy": "proxy.local.com"}
>>> system_proxy = {"proxy": "proxy.global.com"}

>>> config = ChainMap(cmd_proxy, local_proxy, system_proxy)
>>> config["proxy"]
'proxy.local.com'

ChainMap позволяет определить соответствующий приоритет для конфигурации прокси приложения. При поиске ключа выполняется поиск cmd_proxy, затем local_proxy и, наконец, system_proxy, возвращая первый экземпляр рассматриваемого ключа. В примере пользователь не указал прокси в командной строке, поэтому приложение берёт прокси из следующего в списке поставщиков настроек local_proxy.

Управление значениями аргументов по умолчанию

Ещё один вариант использования ChainMap — управление значениями аргументов по умолчанию в методах и функциях. Допустим, вы разрабатываете приложение для управления данными о сотрудниках компании. У вас есть класс обычного, универсального пользователя:

class User:
    def __init__(self, name, user_id, role):
        self.name = name
        self.user_id = user_id
        self.role = role

    # Snip...

В какой-то момент вам необходимо добавить функцию, позволяющую сотрудникам получать доступ к различным компонентам CRM-системы. Ваша первая мысль — изменить User, чтобы добавить новую функциональность. Однако это может сделать класс слишком сложным, поэтому вы решаете создать подкласс CRMUser, чтобы в нём предоставить необходимую функциональность.

В качестве аргументов класс принимает имя пользователя и компонент CRM. Он также принимает **kwargs. Вы хотите реализовать CRMUser таким образом, чтобы обеспечить разумные значения по умолчанию для инициализатора базового класса, не теряя гибкости **kwargs. Вот как проблема решается через ChainMap:

from collections import ChainMap

class CRMUser(User):
    def __init__(self, name, component, **kwargs):
        defaults = {"user_id": next(component.user_id), "role": "read"}
        super().__init__(name, **ChainMap(kwargs, defaults))

В этом примере кода вы создаёте подкласс User. В инициализаторе класса как аргумент вы принимаете имя, компонент и **kwargs. Затем вы создаёте локальный словарь со значениями по умолчанию для user_id и role. Затем вы вызываете метод __.init()__ родительского класса с помощью super(). В этом вызове вы передаёте имя непосредственно инициализатору родителя и предоставляете значения по умолчанию для остальных аргументов с помощью ChainMap.

Обратите внимание, что объект ChainMap как аргументы принимает kwargs, а затем значения по умолчанию. Такой порядок гарантирует, что при создании экземпляра класса предоставленные вручную (kwargs) аргументы будут иметь приоритет над значениями по умолчанию.

Эта статья в подробностях показывает одну из замечательных возможностей Python, а если вы хотите узнать ещё больше, выйти на новый уровень владения Python или научиться программировать с чистого листа, то можете обратить внимание на наш курс о Fullstack-разработке на этом языке или на флагманскую специализацию — Data Science, где Python занимает важное место. Также вы можете узнать, как начать карьеру или прокачаться в других направлениях:

Python, веб-разработка

Data Science и Machine Learning

Мобильная разработка

Java и C#

От основ — в глубину

А также:

Комментарии (2)


  1. azudem
    05.08.2021 06:31
    +1

    Это гугл-перевод?


  1. Roman_Cherkasov
    05.08.2021 18:45

    Promt из 2007 передает привет