Как я могу проверить, к какому подклассу принадлежит экземпляр, не используя isinstance? (Насколько я понимаю, использование isinstance считается плохой практикой?...) Например:
from typing import Protocol
class Entry(Protocol):
name: str
def is_file(self) -> bool: ...
def is_folder(self) -> bool: ...
class File(Entry):
name: str
content: str
def is_file(self) -> bool:
return True
def is_folder(self) -> bool:
return False
def get_size(self) -> int:
return len(self.content)
class Folder(Entry):
name: str
children: list[Entry]
def is_file(self) -> bool:
return False
def is_folder(self) -> bool:
return True
def do_something(entry: Entry) -> None:
if entry.is_file():
print(entry.get_size())
В последней строке do_something средство проверки типов по понятным причинам жалуется, что у entry нет метода с именем get_size, потому что оно явно не понимает, что я использую is_file для того, чтобы убедиться в этом. (Моя проверка типов — Pylance в VSCode) Что я могу сделать вместо этого? (Я знаю, что этот код работает нормально, но я также хотел бы, чтобы он прошел проверку типов)
По крайней мере, в этом примере, почему do_something принимает произвольное Entry, если оно работает только с File объектами?
Неодобрение вызывает не isinstance, а проверка типов во время выполнения в любой форме, вместо таких вещей, как утиная типизация и полиморфизм.
Я согласен с комментариями выше, хотя, если это предполагаемое использование, вы можете использовать typing.cast(File, entry)
Вероятно, нет необходимости в cast; просто используйте if isinstance(entry, File):, и средство проверки типов примет (путем сужения типов), что entry.get_size() допустимо.
Вы должны перепроектировать свою реализацию, чтобы использовать functools.singledispatch.
Это упрощенная версия моего кода, на самом деле в моем коде нет do_something и вместо этого он предназначен для рекурсивной печати папки. Таким образом, он перебирает дочерние элементы данной папки, и если это файл, он печатает этот файл, но если это папка, он вызывает ту же функцию для этой папки. Должен ли я отредактировать код примера, чтобы показать это?
Где именно вы узнали, что isinstance это плохая практика? Это такое широкое заявление, это просто глупо. Я, конечно, видел чрезмерное использование проверки типов во время выполнения, на что ссылается @chepner, но по сути в этом нет ничего плохого. Весь ваш посыл очень сомнительный. Если ваша цель — узнать тип чего-либо, для этого и нужен isinstance. Другой вопрос, нужно ли вам вообще знать тип. Здесь вы просто ищете охранники нестандартного типа, которые заново изобрели бы isinstance колесо.
Не связанное с этим примечание: наследование от протокола с конкретными классами побеждает всю его цель. Протоколы явно предназначены для структурного подтипа, то есть что-то является его подтипом, когда оно соответствует протоколу в своем интерфейсе.
Возможно, более питонический способ справиться с этим — полностью избавиться от is_file и is_folder. Они не что иное, как isinstance под другим именем.
class Entry:
def get_size(self) -> int:
raise NotImplementedError
class File(Entry):
...
def get_size(self) -> int:
return len(self.content)
class Folder(Entry):
...
def do_something(entry: Entry) -> None:
try:
print(entry.get_size())
except NotImplementedError:
pass
Это возлагает слишком много работы на базовый класс, требуя, чтобы он предвосхищал каждый метод, который может добавить подкласс.
@чепнер. Я в основном согласен с вами. Но я не знаю, как этот код будет развиваться. Собирается ли OP добавить больше методов? Или они собираются добавить больше подклассов, некоторые из которых будут иметь разумный метод get_size(). Мне бы очень хотелось узнать больше о методе do_something. . .
Нет причин не использовать здесь isinstance. Это уменьшает количество ненужного шаблонного кода. Нахмурился не isinstance, а ненужная проверка типов во время выполнения любого рода, включая ваши пользовательские is_* методы. Предполагая, что есть веская причина для того, чтобы сделать do_something таким излишне общим, просто используйте следующее.
from typing import Protocol
class Entry:
def __init__(self, *, name: str, **kwargs):
super().__init__(**kwargs)
self.name = name
class File(Entry):
def __init__(self, *, content: str, **kwargs):
super().__init__(**kwargs)
self.content = content
def get_size(self) -> int:
return len(self.content)
class Folder(Entry):
def __init__(self, *, children: list[Entry], **kwargs):
super().__init__(**kwargs)
self.children = children
def do_something(entry: Entry) -> None:
if isinstance(entry, File):
print(entry.get_size())
Однако, если do_something не должен работать ни с чем, кроме объектов File, то укажите это в типе, чтобы вам не нужно было такое сужение типа:
def do_something(entry: File) -> None:
print(entry.get_size())
Независимо от проверки типов, do_something в любом случае кажется слишком общим, но FWIW с использованием isinstance само по себе не является плохой практикой.