В процессе разработки часто приходится использовать словари для получения значения по ключу. Это отлично подходит для маппинга полей различных систем. Например, в одной системе тип документа "Договор", а в другой "Contract". Либо одна система принимает буквенный код валюты "RUB", а другая числовой "643". Для того чтобы они понимали друг друга, необходимо переводить значения в понятные для этой системы, и для этого прекрасно подходят словари.
Я решил создать словари для каждой из систем:
SERVICE_PROVIDER_MAPPING = {
"Договор": "Contract",
"Доп. соглашение": "SupplementaryAgreement",
}
PROVIDER_SERVICE_MAPPING = {
"Contract": "Договор",
"SupplementaryAgreement": "Доп. соглашение",
}
Внешне это выглядит просто, и обратный словарь можно собрать при помощи copy-paste из первого словаря. Это хорошо когда мало значений, но вот дошло дело до кодов валют и их словаря в 160 записей. Сразу пришла в голову идея:
Был бы такой объект в python, в котором происходит маппинг не зависимо от передаваемого ключа. Передаешь RUB получаешь 643, передаешь 643 получаешь RUB
Я подумал об этом и сразу начал искать в интернете что-то подобное. К сожалению, ничего не нашел, но везде рекомендовали просто создать обратный словарь с помощью кода (как я сразу об этом не догадался):
{v: k for k, v in my_dict.items()}
И вот, после длительной работы, я представляю вашему вниманию мой класс SupperMapping. Этот класс позволяет осуществлять маппинг в обе стороны, независимо от того, какой ключ был передан.
class SupperMapping:
"""
Этот класс реализует словарь, которое позволяет получать значения
как по прямым, так и по обратным ключам.
"""
def __init__(
self,
mapping: dict,
default: str | int | None = None,
default_key: str | int | None = None
):
"""
Инициализирует экземпляр класса SupperMapping.
:param mapping: словарь, которое нужно использовать
для инициализации экземпляра класса SupperMapping.
:param default: значение по умолчанию, которое будет возвращаться
методом get, если указанный ключ не будет найден в словаре.
:param default_key: значение ключа по умолчанию,
который будет возвращаться значение методом get, если значение по ключу
не будет найдено.
"""
self._check_default_params(default, default_key)
self.default = default
self.default_key = default_key
self._mapping = mapping
self._reverse_mapping = {
v: k for k, v in self._mapping.items()
}
def __contains__(self, key: str | int) -> bool:
"""
Возвращает True, если указанный ключ присутствует в словаре
или в обратном словаре, и False в противном случае.
:param key: ключ, который нужно проверить на присутствие в словаре.
:return: логическое значение, указывающее,
присутствует ли указанный ключ в словаре.
"""
for target_dict in (self._mapping, self._reverse_mapping):
_, in_dict = self._key_in_dict(key, target_dict)
if in_dict:
return True
return False
def __getitem__(self, key: str | int) -> str | int:
"""
Возвращает значение по указанному ключу из словаря
или из обратного словаря.
Если ключ не найден, генерирует исключение KeyError.
:param key: ключ, по которому нужно получить значение.
:return: значение, соответствующее указанному ключу.
"""
for target_dict in (self._mapping, self._reverse_mapping):
key, in_dict = self._key_in_dict(key, target_dict)
if in_dict:
return target_dict[key]
raise KeyError(key)
def get(
self,
key: str | int,
default: str | int | None = None,
default_key: str | int = None
) -> str | int | None:
"""
Возвращает значение по указанному ключу из словаря
или из обратного словаря.
Если ключ не найден, возвращает значение по умолчанию,
указанное в параметрах default или default_key.
Если ни один из этих параметров не указан, возвращает None.
:param key: ключ, по которому нужно получить значение.
:param default: значение по умолчанию, которое будет возвращаться,
если указанный ключ не будет найден в словаре.
:param default_key: ключ по умолчанию для поиска значения из
словаря которое будет возвращаться,
если указанный ключ не будет найден в словаре.
:return: значение, соответствующее указанному ключу,
или значение по умолчанию.
"""
try:
return self[key]
except KeyError:
pass
self._check_default_params(default, default_key)
if default_key:
return self.get(default_key)
if default:
return default
if self.default_key:
return self.get(self.default_key)
return self.default
def _key_in_dict(
self,
key: str | int,
target_dict: dict
) -> tuple[str | int, bool]:
"""
Проверяет, присутствует ли указанный ключ в указанном словаре.
:param key: ключ, который нужно проверить на присутствие в словаре.
:param target_dict: словарь, в котором нужно проверить
наличие указанного ключа.
:return: кортеж, содержащий ключ и логическое значение,
указывающее, присутствует ли ключ в словаре.
"""
try:
key = self._convert_key_type(key, target_dict)
except ValueError:
return key, False
is_in_dict = key in self._mapping or key in self._reverse_mapping
return key, is_in_dict
@staticmethod
def _convert_key_type(key: str | int, target_dict: dict) -> str | int:
"""
Преобразует тип указанного ключа к типу ключей указанного словаря.
Если преобразование невозможно, генерирует исключение ValueError.
:param key: ключ, тип которого нужно преобразовать.
:param target_dict: словарь, ключи которого используются
для определения типа, к которому нужно преобразовать
указанный ключ.
:return: преобразованный ключ.
"""
mapping_key_type = type(next(iter(target_dict.keys())))
if not isinstance(key, mapping_key_type):
try:
key = mapping_key_type(key)
except Exception as err:
raise ValueError(f"Invalid key type: {err}")
return key
@staticmethod
def _check_default_params(*args):
"""
Проверяет, были ли указаны оба параметра default и default_reverse.
Если оба параметра указаны, генерирует исключение ValueError.
:param args: список параметров, которые нужно проверить
на наличие вместе
:return: None
"""
if all(args):
raise ValueError(
"Cannot specify both "
"default and default_reverse "
"arguments together"
)
Я постарался подробно описать методы и их предназначение.
Пример использования
mapping_dict = {
1: 'one',
2: 'two',
3: 'three'
}
digit_mapping = SupperMapping(mapping_dict)
# Проверка наличия ключа
assert 1 in digit_mapping
assert 'one' in digit_mapping
assert 4 not in digit_mapping
assert 'four' not in digit_mapping
# Получение значения по ключу
assert digit_mapping[1] == 'one'
assert digit_mapping['two'] == 2
assert digit_mapping['2'] == 'two'
assert digit_mapping.get('2') == 'two'
assert digit_mapping.get(4) == None
# Получение значения по умолчанию, если ключ не найден
assert digit_mapping.get(4, 'five') == 'five'
assert digit_mapping.get('four', 2) == 2
assert digit_mapping.get('four', default_key=2) == 'two'
Это начальный вариант, думаю потом прикрутить еще больше фишек. Буду рад замечаниям и советам. Если будет потребность в этом классе можно попробовать и библиотеку на PIP выложить)))
Комментарии (14)
9982th
06.04.2024 11:58Например, в одной системе тип документа "Договор", а в другой "Contract".
одна система принимает буквенный код валюты "RUB", а другая числовой "643"
А что вы будете делать, когда систем станет три?
cement-hools Автор
06.04.2024 11:58там получается у меня одна система, главный сервис, который управляет разными провайдерами. И если появися еще один провайдер, я просто сделаю для него класс маппинга. получится чтобы передать данные в провайдер, я буду для каждого конвертировать модель с участвием мапинга
nickolaym
06.04.2024 11:58+1160 элементов - это всё ещё "мало". Были бы сотни тысяч, можно было бы подумать об эффективном хранении и из-за этого - об эффективном доступе. А тут...
Отдельный минус подхода - смешивание ключей туда-обратно. В языке со слабой типизацией можно накосячить с лёгкостью необычайной. Лучше соблюдать чистоту рук, однако!
Но кстати, быстрое-и-грязное решение - это напихать ключи в один словарь
def make_bimap(src): # src - dict dst = {} dst.update(src) dst.update((v, k) for (k, v) in src.items()) return dst
Но при этом надо удостовериться, что нет конфликта ключей в обратном направлении.
Ну и не забываем про наследие предков. Бимапы люди делали издревле, и быстрый поиск даёт, например, PyPy.bidict.
cement-hools Автор
06.04.2024 11:58я наверное не знал что это бимап называтся, того и не нашел нечего))). Но все ровно было интересно по изобретать
dyadyaSerezha
06.04.2024 11:58Так-то каждый может. А вот сделайте такую мапу, у которой спрашиваешь про новый договор: m.get("договор №18"), а она возвращает: "составлен, одобрен, подписан и уже завершён, деньги уже на вашем счету". Вот это я понимаю.
danilovmy
06.04.2024 11:58+1Скорее всего, что подход "би"-маппинга стоит решать на уровне получения объектов из БД. Это работает, если возможно контролировать БД всех систем.
В остальных случаях идея может быть улучшена, если известна система "источник" и система "назначение", а они известны в текущем примере, поскольку в этом случае маппинг выполняется с помощью только одного словаря "sourse: destination" и исключено появление каких-либо ошибок по ключам, о которых писали предыдущие комментаторы.
PS. Код не работает, если values в
mapping: dict
не hashable. А на __init__ это не проверяется.cement-hools Автор
06.04.2024 11:58не создастся класс тогда, ошибка словаря зарайзиртся
danilovmy
06.04.2024 11:58Верно. Но этот Exception в коде никак не обрабатывается. Я это имел ввиду под "не работает".
googoosik
06.04.2024 11:58+1Был бы такой объект в python, в котором происходит маппинг не зависимо
от передаваемого ключа. Передаешь RUB получаешь 643, передаешь 643
получаешь RUBОн есть.
from enum import Enum
class Currencies(Enum):RUB = 840
Currencies['RUB']
Out[8]: <_Currencies.RUB: 840>
Currencies(840)
Out[10]: <_Currencies.RUB: 840>cement-hools Автор
06.04.2024 11:58Прикольно, не знал что он так работает. А он значения отдаст тоже как хэш-таблица? Почитал исходники, не совсем понял как он работает. По сути то что мне и нужно, но если бы я знал, не придумал бы сам такой велосипед))). У Enum по разному надо вызывать метод чтобы получить значения (Currencies['RUB'], Currencies(840)) а меня однообразно (может это преимущество или недостаток)))
assert digit_mapping['two'] == 2 assert digit_mapping[2] == 'two' assert digit_mapping['2'] == 'two'
googoosik
06.04.2024 11:58Не уверен, что правильно понял Ваш вопрос, но хранит енам данные в дикте, да.
from enum import Enum class Currencies(Enum): RUB = 840 Currencies.__members__ Out[6]: mappingproxy({'RUB': <Currencies.RUB: 840>}) rub = Currencies.__members__['RUB'] rub.value Out[8]: 840 rub.name Out[9]: 'RUB'
Nikolaev_Nikolay
06.04.2024 11:58Скажите а как сделать так, чтобы f(a, b)=c где с линейная зависимость от некой функции в которую входят а и b!? То есть а и b подставляем в словарь и он выдает с который монотонно возрастает от а и b и линейный сам по себе!?
nsaktaganov
Может имеет смысл добавить режим, в котором проверяется наличие коллизии между ключами начального/прямого и обратного словаря? Например, можно выбрасывать исключение в таком случае. И добавить проверку, что все значения прямого словаря уникальные. И также выбрасывать исключение в случае обнаружения дубликатов.
cement-hools Автор
кстати да, спасибо, не подумал об этом. Хотя может список отдавать значений, чтобы потом выбрать можно было. но тогда не понятно будет что ожидать, или значение или список, проверки дополнительные со значение проводить. Есть еще над чем подумать