Класс Reverse Mapping на Python

a90dd6f937ad49ce22fd50cda6c55c26

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

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

© Habrahabr.ru