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