Класс Reverse Mapping на Python

от автора

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

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


ссылка на оригинал статьи https://habr.com/ru/articles/805793/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *