Вот ситуация. Я пытаюсь уменьшить количество шаблонного кода в моем проекте. Вот упрощенная версия установки.
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.
Отредактировано для исправления ошибки в декораторе.
@chepner, спасибо за ответ, я случайно упустил это, когда делал минимальный рабочий пример для обсуждения. Я обновил вопрос. Спасибо!
Два наблюдения: если я добавлю вызов Test().do_something()
в конец скрипта, pyright
тоже на это пожалуется. Но если я заменю TCallable
на Callable[[type], type[B]]
, то pyright
будет жаловаться только на self.do_something
. Это говорит о том, что есть проблема с псевдонимом, но это pyright
также проверяет тип Test
, прежде чем принимать во внимание декоратор.
Это интересное наблюдение.
Мне интересно, возможно ли это вообще. Я просматривал это обсуждение ошибок, и, исходя из этого, кажется, что средства проверки статического типа не смогут сделать вывод из динамического создания класса.
Кажется, что pyright
может использовать @decorator
для контекста так же, как и оператор class
. При этом могут возникнуть проблемы, о которых я не знаю.
Это слишком много метапрограммирования для статической проверки типов. Вам может быть полезен @dataclass_transform
, но он все равно не решит всех ваших проблем. Вы не можете выразить это статически типизируя Python. За подсказку типов приходится платить, и иногда довольно много. Вы можете: а) написать плагин mypy
(не поможет pyright
, но по крайней мере один из крупных специалистов по проверке типов вас поймет), б) отказаться от этого и явно наследовать и в) просто перестать пытаться вписать это в подсказки типов — это не так. обязателен и иногда может навредить красивому дизайну.
Во-первых, я должен отметить, что это можно реализовать без декоратора следующим образом:
@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
. Надеюсь, это полезно!
decorator
нужно вернуться_decorator
. Не уверен, что это единственная проблема.