The List[Geometry]
type hint on the code below does not cause self.items.append(line)
to complain because line
is a Line
which is subclass of Geometry
.
The same type hint causes the item.is_horizontal
on the last line to say Unresolved attribute reference 'is_horizontal' for class 'Geometry'
.
I think this happens because the is_horizontal
property exists only in one of the subclasses of Geometry
.
What is the correct way to avoid this warning?
With List[Union[Geometry, Line, Circle]]
the warning goes away, but I'm not sure this is the correct solution. Listing a set of classes is not like saying <Geometry> and all subclasses
.
class Geometry:
is_circle = is_line = False
class Line(Geometry):
is_line = True # property on Geometry and subclasses
is_horizontal = True # property only on Line
class Circle(Geometry):
is_circle = True # property on Geometry and subclasses
radius = 10 # property only on Line
class Geometries:
def __init__(self):
self.items: List[Geometry] = []
def add_line(self, line: Line):
self.items.append(line)
def horizontal_lines(self):
return [item for item in self.items if item.is_line and item.is_horizontal]
CodePudding user response:
Use an actual type check, with isinstance
:
def horizontal_lines(self):
return [item for item in self.items if isinstance(item, Line) and item.is_horizontal]
From a static typing perspective, there is no connection between an is_line
attribute and whether or not an object is an instance of Line
. There could be other subclasses of Geometry
somewhere else in the program with is_line = True
but no is_horizontal
, or instances could have is_line
set at the instance level.
CodePudding user response:
Since items
has a static type of List[Geometry]
, the type checker cannot make any assumptions about what attributes a list element has beyond what is statically defined in Geometry
. Since is_horizontal
is not an attribute of all instances of Geometry
, an access like item.is_horizontal
is forbidden.
However, you can assert that item
is really a Line
based on runtime knowledge using cast
. cast(Line, item)
is a no-op at runtime, but the type checker treats it as meaning "whatever type item
is, cast
will return an instance of Line
.
Given some more realistic definitions of Geometry
, Line
, and Circle
,
from typing import cast, List
class Geometry:
...
class Line(Geometry):
def __init__(self, slope, y_intercept):
...
@property
def is_horizontal(self):
return self.slope == 0
...
class Circle(Geometry):
def __init__(self, radius, center):
self.radius = radius
self.center = center
...
you can define Geometries
(and in particular, Geometries.horizontal_lines
) like so:
class Geometries:
def __init__(self):
self.items: List[Geometry] = []
def add_line(self, line: Line):
self.items.append(line)
def horizontal_lines(self) -> List[Line]:
return [item for item in self.items
if isinstance(item, Line)
and cast(Line, item).is_horizontal]
Various static type-checkers may make the call to cast
optional. For example, they could infer that given isinstance(x, y) and z
, if isinstance
returns True then the static type of x
can be assumed to be y
in z
, regardless of any previous type hints regarding the static type of x
. (If it were to return False
, then z
is dead code and needn't be checked at all.)