Python: подсказка типов для оформления пользовательского класса данных с помощью наследования

Вот ситуация. Я пытаюсь уменьшить количество шаблонного кода в моем проекте. Вот упрощенная версия установки.

from __future__ import annotations

import dataclasses
import typing as t
import types

_Self = t.TypeVar("_Self", bound = "A")

class A:
  """Base class for all classes"""
  @classmethod
  def create(cls: type[_Self], *args, **kwargs) -> _Self:
    return cls(*args, **kwargs)

  def do_something(self) -> None:
    """a 'void' method"""
    pass


class B(A):
  """A secondary base class for the future dataclasses; NOTE: inheritance from A!"""


T = t.TypeVar("T", bound=B)
TCallable = t.Callable[[type], type[T]]


def decorator(**kwargs) -> TCallable:
  """A decorator for reducing boilerplate"""
  def _decorator(cls: type) -> type[B]:
    return types.new_class(
      cls.__name__, 
      (
        dataclasses.dataclass(**{"frozen": True, "kw_only": True, **kwargs}(cls), 
        B,
      ),
    )
  return _decorator


@decorator(repr=False)
class Test:
   """This is an implementation of the dataclass, subclassed from B, subclassed from A"""
   name: str
   
    def __repr__(self) -> str:
      return self.name
    
    def do_something_else(self) -> None:
      self.do_something() # <- not recognized by pyright

Проблема в том, что моя программа проверки статического типа не может распознать методы из class A в верхней части порядка разрешения внутри экземпляров class Test.

Я использую Pylance/Pyright в VSCode.

Отредактировано для исправления ошибки в декораторе.

decoratorнужно вернуться _decorator. Не уверен, что это единственная проблема.
chepner 18.04.2024 20:22

@chepner, спасибо за ответ, я случайно упустил это, когда делал минимальный рабочий пример для обсуждения. Я обновил вопрос. Спасибо!

Alex 18.04.2024 20:27

Два наблюдения: если я добавлю вызов Test().do_something() в конец скрипта, pyright тоже на это пожалуется. Но если я заменю TCallable на Callable[[type], type[B]], то pyright будет жаловаться только на self.do_something. Это говорит о том, что есть проблема с псевдонимом, но это pyright также проверяет тип Test, прежде чем принимать во внимание декоратор.

chepner 18.04.2024 20:40

Это интересное наблюдение.

Alex 18.04.2024 20:45

Мне интересно, возможно ли это вообще. Я просматривал это обсуждение ошибок, и, исходя из этого, кажется, что средства проверки статического типа не смогут сделать вывод из динамического создания класса.

Alex 18.04.2024 20:59

Кажется, что pyright может использовать @decorator для контекста так же, как и оператор class. При этом могут возникнуть проблемы, о которых я не знаю.

chepner 18.04.2024 21:09

Это слишком много метапрограммирования для статической проверки типов. Вам может быть полезен @dataclass_transform, но он все равно не решит всех ваших проблем. Вы не можете выразить это статически типизируя Python. За подсказку типов приходится платить, и иногда довольно много. Вы можете: а) написать плагин mypy (не поможет pyright, но по крайней мере один из крупных специалистов по проверке типов вас поймет), б) отказаться от этого и явно наследовать и в) просто перестать пытаться вписать это в подсказки типов — это не так. обязателен и иногда может навредить красивому дизайну.

STerliakov 19.04.2024 03:46
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
7
69
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Во-первых, я должен отметить, что это можно реализовать без декоратора следующим образом:

@dataclasses.dataclass(frozen = True, kw_only = True, repr=False)
class Test(B):
   """This is an implementation of the dataclass, subclassed from B, subclassed from A"""
   name: str
   
   def __repr__(self) -> str:
      return self.name
    
   def do_something_else(self) -> None:
      self.do_something()

Но давайте предположим, что по какой-то причине оно должно быть там. В этом примере предполагается, что вы просто хотели избежать многократного ввода frozen и kw_only:

from __future__ import annotations

import dataclasses
import typing as t

_Self = t.TypeVar("_Self", bound = "A")

class A:
  """Base class for all classes"""
  @classmethod
  def create(cls: type[_Self], *args, **kwargs) -> _Self:
    return cls(*args, **kwargs)

  def do_something(self) -> None:
    """a 'void' method"""
    pass

class B(A):
  """A secondary base class for the future dataclasses; NOTE: inheritance from A!"""

@t.dataclass_transform()
def decorator(**kwargs):
  def _decorator(cls: type) -> type[B]:
    return dataclasses.dataclass(**{"frozen": True, "kw_only": True, **kwargs})(cls)
  return _decorator


@decorator(repr=False)
class Test(B):
   """This is an implementation of the dataclass, subclassed from B, subclassed from A"""
   name: str
   
   def __repr__(self) -> str:
      return self.name
    
   def do_something_else(self) -> None:
      self.do_something()

В настоящее время невозможно добавить аспект наследования в тип, поэтому я оставил его наследование от B. В общем, в любом случае это, вероятно, лучшая практика, поскольку она делает структуру наследования более понятной. Однако со временем это можно будет ввести с помощью Intersection. Надеюсь, это полезно!

Другие вопросы по теме