Как сделать словарь к которому можно обращаться через точку

Необходимо написать словарь к которому можно обращаться как к словарю aiogram'a.

Пример, есть словарь genMessage - это самый обычный словарь:

genMessage = {
        'from_user': {
            'id': userId,
            'username': username,
            'full_name': fullName
        }
    }

И к этому словарю можно обращаться только так:

genMessage['from_user']['id']

А мне необходимо обращаться к словарю через точку, как к aiogram словарю, пример:

genMessage.from_user.id

Ответы (2 шт):

Автор решения: gil9red

Накидал пример класса-обертки:

  • Суть в магическом методе __getattr__, который вызывается при запросе поля-атрибута у объекта
  • Например, при x.from_user в __getattr__ будет передана строка from_user
  • Чтобы можно было продолжать цепочку вызова понадобится возвращать объект WrapperMap с новым значением

Пример:

class WrapperMap:
    def __init__(self, d: dict):
        self.d = d

    def get_value(self):
        return self.d

    def __getattr__(self, item: str) -> 'WrapperMap':
        return self.__class__(self.d.get(item))

    def __repr__(self):
        return repr(self.d)


genMessage = {
    'from_user': {
        'id': 123,
        'username': "username",
        'full_name': "fullName"
    }
}
x = WrapperMap(genMessage)
print(x.from_user.id, type(x.from_user.id))
# 123 <class '__main__.WrapperMap'>

print(x.from_user.username.get_value())
# username

Можно немного дописать __getattr__, чтобы возвращать обертку только для словарей:

    def __getattr__(self, item: str):
        value = self.d.get(item)
        if isinstance(value, dict):
            return self.__class__(value)
        
        return value

Тогда для получения значений остальных типов не придется вызывать метод get_value:

x = WrapperMap(genMessage)

print(x.from_user, type(x.from_user))
# {'id': 123, 'username': 'username', 'full_name': 'fullName'} <class '__main__.WrapperMap'>

print(x.from_user.id, type(x.from_user.id))
# 123 <class 'int'>

print(x.from_user.username, type(x.from_user.username))
# username <class 'str'>
→ Ссылка
Автор решения: insolor

"Энтерпрайзный" вариант: описываем "схему" словаря/json, т.е. все объекты, которые встречаются внутри словаря, и отношения между ними, потом парсим словарь в эти объекты, и потом уже обращаемся к полям этих объектов через точку.

Один из плюсов такого варианта - IDE будет знать, какие поля у этих объектов есть, и сможет подсказать поле через автодополнение. Дополнительно, в классах можно прикрутить валидацию значений (через тот же pydantic), и, например, если с клиента передаются некорректные данные, сервер сможет это проконтролировать, и вернуть на клиент ошибку с конкретным описанием проблемы.

Пример на pydantic (Python 3.6+, pip install pydantic):

from pydantic import BaseModel


class User(BaseModel):
    id: int
    username: str
    full_name: str


class Message(BaseModel):
    from_user: User


genMessage = {
    'from_user': {
        'id': 1,
        'username': 'johndoe',
        'full_name': 'John Doe'
    }
}


message = Message.parse_obj(genMessage)

print(message.from_user.id)

Пример на dataclasses ("из коробки" в Python 3.7+ или установить через pip install dataclasses на Python 3.6) + dacite (pip install dacite):

from dataclasses import dataclass
from dacite import from_dict


@dataclass
class User:
    id: int
    username: str
    full_name: str


@dataclass
class Message:
    from_user: User


genMessage = {
    'from_user': {
        'id': 1,
        'username': 'johndoe',
        'full_name': 'John Doe'
    }
}


message = from_dict(data_class=Message, data=genMessage)

print(message.from_user.id)
→ Ссылка