Reverse Mapping Class in Python

During the development process, you often need to use dictionaries to get the value of a key. This is great for mapping fields of different systems. For example, in one system the document type is “Agreement”, and in another “Contract”. Or one system accepts the alphabetic currency code “RUB”, and the other the numeric code “643”. In order for them to understand each other, it is necessary to translate the meanings into understandable ones for this system, and dictionaries are great for this.

I decided to create dictionaries for each of the systems:

SERVICE_PROVIDER_MAPPING = {
    "Договор": "Contract",
    "Доп. соглашение": "SupplementaryAgreement",
}


PROVIDER_SERVICE_MAPPING = {
    "Contract": "Договор",
    "SupplementaryAgreement": "Доп. соглашение",
}

Outwardly it looks simple, and the reverse dictionary can be assembled using copy-paste from the first dictionary. This is good when there are few values, but now it comes to currency codes and their dictionary of 160 entries. An idea immediately came to mind:

There would be such an object in python in which mapping occurs regardless of the transmitted key. If you transfer RUB you get 643, if you transfer 643 you get RUB

I thought about it and immediately started looking on the Internet for something similar. Unfortunately, I didn’t find anything, but everywhere they recommended just creating a reverse dictionary using code (I didn’t immediately guess about it):

{v: k for k, v in my_dict.items()}

And now, after much work, I present to your attention my SupperMapping class. This class allows mapping in both directions, regardless of which key was passed.

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(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"
            )

I tried to describe in detail the methods and their purpose.

Usage example

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'

This is the initial option, I think I’ll add even more features later. I will be glad to receive comments and advice. If there is a need for this class, you can try and post the library on PIP)))

link to repository

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *