Класс Reverse Mapping на Python

Kate

Administrator
Команда форума
В процессе разработки часто приходится использовать словари для получения значения по ключу. Это отлично подходит для маппинга полей различных систем. Например, в одной системе тип документа "Договор", а в другой "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 выложить)))

ссылка на репозиторий

 
Сверху