How can I check which subclass an instance belongs to, without using isinstance
?
(My understanding is that using isinstance
is considered bad practice?...)
For example:
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())
On the last line of do_something
the type checker understandably complains that entry
has no method named get_size
, because it obviously doesn't understand that I use is_file
to to ensure that. (My type checker is Pylance in VSCode)
What can I do instead?
(I know this code works fine, but I would also like for it to pass type checking)
CodePudding user response:
There's no reason not to use isinstance
here. It reduces the amount of unnecessary boilerplate. It isn't isinstance
that is frowned up, but unnecessary run-time typechecking of any kind, including your custom is_*
methods. Assuming there is a good reason for making do_something
so unnecessarily generic, just use the following.
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())
However, if do_something
isn't supposed to work on anything except File
objects, then say so in the type so you don't need any such type narrowing:
def do_something(entry: File) -> None:
print(entry.get_size())
CodePudding user response:
A possibly more Pythonic way to handle this is to get rid of the is_file
and is_folder
completely. They're nothing but isinstance
by another name.
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