What’s the right way to add a method to any member of a set of sibling instances (i.e. each instance’s class inherits from a common base class)?
The siblings come from fsspec
and are out of my control, but it’s like this.
class Base:
def open(self): pass
def read(self): pass
class A(Base):
def read(self): pass
class B(Base):
def read(self): pass
There could be a C
and some plugin might add a D
… these are out of my control. All my code gets is an instance of some subclass of Base
.
My code gets an instance and needs to hand it off to another library expecting an instance of a class inheriting from Base
. But I want this instance to have a new feature (not something that would be of general utility outside my code, so no changes to fsspec
). In code, something like this.
with fsspec.open(...) as f: # f is an instance of `A`, `B`, etc.
g = dunno_what(f) # asking about this part
dataset = xarray.open_dataset(g) # handing off to xarray
Now xarray
is going to behave differently depending on what g
implements, and requires g
to be an instance of Base
.
The way I see it, my feature would come as a method of a mixin.
class Mixin:
def feature(self): pass
Then the following appears to achieve my goals but has lots of drawbacks.
for Item in (A, B):
# `item` is the `f` in the minimal example above
item = Item()
# here's what I've tried, but is advised against
item.__class__ = type("Item", (item.__class__, Mixin), {})
# it works though ...
assert isinstance(item, Base)
assert item.open.__func__ is Base.open
assert item.read.__func__ is Item.read
assert item.feature.__func__ is Mixin.feature
Assigning to item.__class__
is not for novices, so I am interested in the suggested alternatives given by abarnert (third one is outside of my control):
- “Use a factory to create an instance of the appropriate class dynamically, instead of creating a base instance and then munging it into a derived one.”
- “Use
__new__
or other mechanisms to hook the construction.”
I can’t think of how to do either one while still achieving the goals given above as assertions. Thank you for showing me the way!
10
You can create a proxy object for the given item and mixin class with Base
as the base class such that the proxy object would delegate attribute lookups to the item and then to the mixin class as a fallback:
def add_mixin(item, mixin):
class _Proxy(Base):
def __getattribute__(self, name):
try:
return getattr(item, name)
except AttributeError:
return getattr(mixin, name).__get__(item)
return _Proxy()
for Item in (A, B):
item = Item()
item = add_mixin(item, Mixin)
assert isinstance(item, Base)
assert item.open.__func__ is Base.open
assert item.read.__func__ is Item.read
assert item.feature.__func__ is Mixin.feature
Demo: https://ideone.com/JVRzyO
Note that the factory function above dynamically creates a class with the item and the mixin in a closure for each item, which incurs a higher overhead.
A leaner approach would be to use a proxy class whose constructor takes an item and a mixin class, though the resulting code would be somewhat less readable because the instance variables can’t be accessed with the dot operator but with the __dict__
attribute, which itself has to be obtained by calling object.__attribute__
instead:
class Proxy(Base):
def __init__(self, item, mixin):
self.item = item
self.mixin = mixin
def __getattribute__(self, name):
try:
return getattr(
item := object.__getattribute__(self, '__dict__')['item'], name
)
except AttributeError:
return getattr(
object.__getattribute__(self, '__dict__')['mixin'], name
).__get__(item)
for Item in (A, B):
item = Item()
item = Proxy(item, Mixin)
assert isinstance(item, Base)
assert item.open.__func__ is Base.open
assert item.read.__func__ is Item.read
assert item.feature.__func__ is Mixin.feature
Demo: https://ideone.com/eiyV2j
As suggested by @juanpa.arrivillaga though, this approach can be made more readable while minimizing the potential of name collision by making the instance variables of the proxy class “private” through the protection of name mangling with a prefix of double underscores:
class Proxy(Base):
def __init__(self, item, mixin):
self.__item = item
self.__mixin = mixin
def __getattr__(self, name):
try:
return getattr(self.__item, name)
except AttributeError:
return getattr(self.__mixin, name).__get__(name)
for Item in (A, B):
item = Item()
item = Proxy(item, Mixin)
assert isinstance(item, Base)
assert item.open.__func__ is Base.open
assert item.read.__func__ is Item.read
assert item.feature.__func__ is Mixin.feature
assert item.__Proxy_item is item
assert item.__Proxy_mixin is Mixin
Demo: https://ideone.com/OJ42Ge
9