Типовые подсказки при представлении ключей dict с помощью нотации атрибутов в python 3.6

Учитывая пример первого ответа в Доступ к ключам dict как к атрибуту?:

class AttrDict(dict):
    def __init__(self, *args, **kwargs):
        super(AttrDict, self).__init__(*args, **kwargs)
        self.__dict__ = self

и функция, которая возвращает:

def dict_to_attrdict(somedict):
    return AttrDict(**somedict)

назначается как:

data = dict_to_attrdict(mydict)

Как правильно добавить подсказки типа для класса и функции, которые будут проходить проверку mypy с учетом следующих ограничений:

  • ключи dict всегда будут str
  • значения dict должны быть динамическими и представлены Any, поскольку они различаются, поэтому я не хочу вводить каждое по отдельности, то есть некоторые str, List[dict[str, List]], Dict[str, str], Dict[str, List]

person 264nm    schedule 04.07.2018    source источник


Ответы (1)


Вы можете сделать сами определения классов и функций, выполнив следующие действия:

from typing import Dict, Any

class AttrDict(Dict[str, Any]):
    def __init__(self, *args: Any, **kwargs: Any) -> None:
        super(AttrDict, self).__init__(*args, **kwargs)
        self.__dict__ = self

def dict_to_attrdict(some: Dict[str, Any]) -> AttrDict:
    return AttrDict(**some)

Наследование от Dict[X, Y] ничем не отличается от наследования только от dict во время выполнения, но оно дает mypy дополнительные метаданные, в которых он нуждается.

Однако на самом деле вы не сможете использовать экземпляр AttrDict безопасным для типов способом: mypy всегда будет помечать такие вещи, как my_attrdict.foo, как ошибку.

Это связано с тем, что невозможно статически определить, какие поля будут присутствовать в AttrDict во всех случаях — mypy понятия не имеет, что именно находится внутри AttrDict. И поскольку mypy не может сказать, действительно ли безопасно делать такие вещи, как my_attrdict.foo, он склоняется к консервативной стороне и просто решает считать это небезопасным.

У вас есть два разных варианта решения этой проблемы. Во-первых, если вы действительно хотите, чтобы AttrDict был как можно более динамичным, вы можете сказать mypy, чтобы он просто предполагал, что тип является любым произвольным динамическим типом, например:

from typing import Dict, Any, TYPE_CHECKING

if TYPE_CHECKING:
    AttrDict = Any
else:
    class AttrDict(dict):
        def __init__(self, *args, **kwargs) -> None:
            super(AttrDict, self).__init__(*args, **kwargs)
            self.__dict__ = self

def dict_to_attrdict(some: Dict[str, Any]) -> AttrDict:
    return AttrDict(**some)

TYPE_CHECKING — это значение, которое всегда равно False во время выполнения, но mypy всегда обрабатывает его как True. Конечным результатом является то, что mypy будет рассматривать только ветвь «if» этого if/else и игнорировать все, что находится в ветке «else»: теперь мы научили mypy, что AttrDict является псевдонимом типа для Any : точно эквивалентно Any. Однако во время выполнения мы всегда попадаем в ветку else и определяем класс, как раньше.

Основным недостатком этого подхода является то, что мы действительно не получаем никакой пользы от использования статической типизации. Мы можем добавить немного безопасности к dict_to_attrdict, так как теперь мы можем принудительно указать, что ключи должны быть строками, но это все.

Второй вариант — использовать сильные стороны mypy и переписать свой код, чтобы он действительно использовал классы. Итак, мы бы избавились от AttrDict и фактически использовали бы классы, которые устанавливают свои поля.

Это позволяет mypy понять, какие поля присутствуют, каковы их типы и т. д. Это немного больше работы заранее, но плюс в том, что mypy может дать вам более сильные гарантии правильности вашего кода.

Если вы считаете, что на самом деле определение группы классов с полями утомительно, попробуйте использовать новый модуль «dataclasses» (если вы используете Python 3.7) или сторонний модуль «attrs». Я считаю, что mypy недавно добавил поддержку для обоих.

Возможно, вам придется подождать, пока mypy 0.620 не будет выпущена в предстоящий вторник, если вы хотите использовать классы данных — я не помню, вошла ли эта функция в mypy 0.600 или mypy 0.610.

person Michael0x2a    schedule 05.07.2018
comment
Спасибо за объяснение! В итоге я выбрал немного другой подход. Я заменил этот класс на тот, который устанавливает его рекурсивно и также обрабатывает вложенные списки, но все же выбрал подход «Dict[str, Any]», как указано выше в вашем ответе. Затем я создал класс NamedTuple, который определил типы вложенных структур, которые я действительно хотел использовать из словаря. Таким образом, атрибуты могут иметь свои типы, сохраняя гибкость, которую я хотел в первую очередь для других частей кода, но не нарушая цель статической типизации. - person 264nm; 06.07.2018