Proposal to remove `Mobject.submobjects`, allow only Groups to have `submobjects`, and define `get_submobjects()` to expose submobjects
This proposal is related to the issue #4339 and attempts to solve the described problem in a different way:
Description of problem
Sometimes we store child mobjects of some composed mobject as attributes, while also
.add-ing them to theself.submobjectslist directly.This is fine in principle, but has the odd side effect of producing "mismatches" when a user updates the attribute by reassinging it, because the actually for rendering relevant
self.submobjectslist still holds a reference to the former attribute value.As an example:
sq = Square() b = BraceText(sq, "hello") # this stores the label in `b.label` b.label = Text("new value") # and here we reassign the label ...This example still renders as a brace with label
"hello", because the reference in theself.submobjectslist is never updated.
Proposed solution
The idea is the same: to have a single reference to each submobject, a single source of truth for them.
It does not make a lot of sense that "basic" Mobjects such as Circle, Square, ImageMobject or ValueTracker have submobjects. Instead, it makes more sense to take a Circle and group it inside a Group or VGroup containing other Mobjects. This leads me to the idea that maybe only Groups should have submobjects as such.
Therefore, my idea is:
- The base class
Mobjectdoes not have.submobjects. It also does not have methods such as.add(),.remove(), etc. - Only
Group,VGroupand theirs subclasses have.submobjects,.add(),.remove(), etc. - Groups should be a special Mobject which is more clearly separated from the rest of Mobjects. For example, currently
Axesis a subclass ofVGroupandCoordinateSystem. It should not be a subclass ofVGroup.Axesshould have another way of exposing its submobjects and should not have.submobjects,.add(),.remove(), etc. See a more detailed description of how to achieve that below.
In this way, if only Groups have submobjects and you can only directly add submobjects to Groups, then we put a stricter limit to the unexpected behaviors which might arise from, say, adding a Mobject which already exists.
We still need Mobjects that contain other Mobjects without necessarily being Groups, like MathTex and Axes. Therefore, I propose defining a Mobject.get_submobjects() or Mobject.get_children() method.
- For the base class
Mobject, this should simply return[]. - Other Mobjects which need children may override it. For example,
Axes.get_children()would return[self.x_axis, self.y_axis].NumberLine.get_children()would return something like[*super().get_children(), *self.ticks, *self.numbers]. - Of course,
Circledoes not override the method, so it still returns[]. Group.get_children()andVGroup.get_children()simply returnself.submobjects.
In this way, by getting rid of .submobjects where necessary and making .get_submobjects() or .get_children() return the already existing references to those submobjects, we ensure that there's effectively one single reference for each one of them, solving the described problem.
While it does constitute a severe breaking change in contrast to #4339, I still kind of like the idea. We should probably check
- where in the library things are being
.added to non-group mobjects, - and if all of these instances can easily be replaced.
I would assume that users primarily use VGroup to add/remove submobjects to (which would still be fine, if I understand your proposal correctly), but it would also be nice to try and get more input from perhaps the Discord or so.
To reduce the gap with the current implementation, we could keep an internal list of additional submobjects:
class Mobject:
_extra_mobjects: list[Mobject]
@property
def submobjects(self, /) -> _SubMobjectsAccessor:
return _SubMobjectsAccessor(self)
@submobjects.setter
def submobjects(self, new_submobjects: Iterable[Mobject], /) -> None:
self.submobjects.clear()
self.submobjects.extend(new_submobjects)
where _SubMobjectsAccessor is a class with the same interface as list, but uses the new interface proposed above and the _extra_mobjects attribute for things like .append(...) and similar. Because it's a custom class, it can be smart about which Mobjects to add (to avoid, for example, including the same Mobject multiple times): I think this would make the new proposal almost (if not fully) backwards-compatible.
Edit: This would also allow a more incremental adoption of the new API in the whole library. And of course, once we've migrated most of the library to the new API, we can deprecate and then remove submobjects and _SubMobjectsAccessor.
I know graphs and matrices .add submobjects to themselves despite inheriting from VMobject.