Как следует использовать динамически определяемые атрибуты экземпляра с подсказками типа / linting?

#python #inheritance #static-analysis #type-hinting

#python #наследование #статический анализ #подсказка типа

Вопрос:

Рассмотрим следующий код, Child от которого наследуется мой класс Parent (класс, полученный из стороннего пакета, над которым я не имею никакого контроля). Parent имеет встроенный метод set_attributes , который используется для добавления произвольных атрибутов к экземпляру:

 # third-party code, I can't modify this
class Parent:
    def __init__(self, a: str) -> None:
        self.a = a

    def set_attributes(self, **kwargs) -> None:
        for k, v in kwargs.items():
            setattr(self, k, v)


# my code, I can modify this
class Child(Parent):
    def __init__(self, a: str, b: int, c: float) -> None:
        super().__init__(a=a)
        self.set_attributes(b=b, c=c)

    def show_attributes(self):
        print(self.a)
        print(self.b)
        print(self.c)
 

Это работает нормально, но любой линтер выдаст мне предупреждение о show_attributes методе, сообщая мне, что self.b и self.c не существуют (потому что они определены во время выполнения, когда self.set_attributes(b=b, c=c) выполняется строка).

Один из способов преодолеть это ограничение — определить заполнитель для атрибутов в Child.__init__ :

 from typing import Optional

class Child(Parent):
    def __init__(self, a: str, b: int, c: float) -> None:
        super().__init__(a=a)
        self.b: Optional[int] = None
        self.c: Optional[float] = None
        self.set_attributes(b=b, c=c)
 

… но проблема сейчас в том, что self.b и self.c на самом деле не должны быть None значениями — это просто заполнитель. Я не хочу, чтобы мой линтер рассматривал возможность того, что эти значения могут быть None для остальной части моего кода, потому что их не должно быть.

Другие альтернативы, о которых я думал::

  • Удалить Optional из подсказки типа — теперь линтер предупреждает меня, что self.b и self.c не должно быть None :
 class Child(Parent):
    def __init__(self, a: str, b: int, c: float) -> None:
        super().__init__(a=a)
        self.b: int = None
        self.c: float = None
        self.set_attributes(b=b, c=c)
 
  • Используйте значение по умолчанию соответствующего типа: это работает, но кажется банальным и многословным (я чувствую, что это укусит меня позже).:
 class Child(Parent):
    def __init__(self, a: str, b: int, c: float) -> None:
        super().__init__(a=a)
        self.b: int = 0
        self.c: float = 0.0
        self.set_attributes(b=b, c=c)
 
  • введите намек на атрибуты, но не указывайте им значение-заполнитель: это выглядит хорошо, но компоновщик по-прежнему считает, что эти атрибуты не существуют, даже после выполнения set_attributes метода:
 class Child(Parent):
    def __init__(self, a: str, b: int, c: float) -> None:
        super().__init__(a=a)
        self.b: int
        self.c: float
        self.set_attributes(b=b, c=c)
 

Ни одно из этих решений меня не устраивает. Кроме того, ни одно из этих решений не работает, если я решу сохранить гибкость set_attributes путем добавления **kwargs Child.__init__ , т.е.:

 class Child(Parent):
    def __init__(self, a: str, b: int, c: float, **kwargs) -> None:
        super().__init__(a=a)
        self.b: int
        self.c: float
        self.set_attributes(b=b, c=c, **kwargs)
 

Есть ли лучший способ обойти это, или это просто ограничение, к которому я должен привыкнуть?

Комментарии:

1. динамически определяемые атрибуты по своей сути противоречат номинальной типизации.

2. @pavel Я не вижу никакой двусмысленности, они используют Optional[int] и Optional[float] что было бы довольно идиоматичным способом связывания Union[None, int] и Union[None, float] .