Как я могу добавить аннотации типов к динамически создаваемым классам?

В одном приложении у меня есть код, который генерирует динамические классы, что значительно уменьшает количество дублированного кода. Но добавление подсказок типа для проверки mypy привело к ошибке. Рассмотрим следующий пример кода (упрощенный, чтобы сосредоточиться на соответствующих битах):

class Mapper:

    @staticmethod
    def action() -> None:
        raise NotImplementedError('Not yet implemnented')


def magic(new_name: str) -> type:

    cls = type('%sMapper' % new_name.capitalize(), (Mapper,), {})

    def action() -> None:
        print('Hello')

    cls.action = staticmethod(action)
    return cls


MyCls = magic('My')
MyCls.action()

Проверка этого с помощью mypy приведет к следующей ошибке:

dynamic_type.py:15: error: "type" has no attribute "action"
dynamic_type.py:21: error: "type" has no attribute "action"

mypy, очевидно, не может определить, что возвращаемое значение из вызова type является подклассом Mapper, поэтому он жалуется, что "тип" не имеет атрибута "действие", когда я ему присваиваю.

Обратите внимание, что код работает отлично и делает то, что должен, но mypy по-прежнему жалуется.

Есть ли способ пометить cls как тип Mapper? Я попытался просто добавить # type: Mapper в строку, создающую класс:

cls = type('%sMapper' % new_name.capitalize(), (Mapper,), {})  # type: Mapper

Но тогда я получаю следующие ошибки:

dynamic_type.py:10: error: Incompatible types in assignment (expression has type "type", variable has type "Mapper")
dynamic_type.py:15: error: Cannot assign to a method
dynamic_type.py:15: error: Incompatible types in assignment (expression has type "staticmethod", variable has type "Callable[[], None]")
dynamic_type.py:16: error: Incompatible return value type (got "Mapper", expected "type")
dynamic_type.py:21: error: "type" has no attribute "action"

person exhuma    schedule 11.01.2018    source источник


Ответы (1)


Одно из возможных решений состоит в следующем:

  1. Введите вашу magic функцию с ожидаемыми типами ввода и вывода
  2. Оставьте содержимое вашей magic функции динамически типизированным с разумным использованием Any и # type: ignore

Например, сработает что-то вроде этого:

class Mapper:
    @staticmethod
    def action() -> None:
        raise NotImplementedError('Not yet implemnented')


def magic(new_name: str) -> Mapper:

    cls = type('%sMapper' % new_name.capitalize(), (Mapper,), {})

    def action() -> None:
        print('Hello')

    cls.action = staticmethod(action)  # type: ignore
    return cls  # type: ignore


MyCls = magic('My')
MyCls.action()

Может показаться немного неприятным оставлять часть вашей кодовой базы динамически типизированной, но в данном случае я не думаю, что этого можно избежать: mypy (и экосистема типизации PEP 484) намеренно не пытается обрабатывать супердинамический код вроде это.

Вместо этого лучшее, что вы можете сделать, - это четко задокументировать «статический» интерфейс, добавить модульные тесты и ограничить динамические части вашего кода как можно меньшим размером области.

person Michael0x2a    schedule 12.01.2018
comment
Думаю, это надо будет сделать;) - person exhuma; 17.01.2018
comment
Фактически, определение волшебной функции должно иметь возвращаемый тип typing.Type[Mapper]. Таким образом можно удалить type: ignore в строке return. - person exhuma; 19.09.2018