alphazwest
alphazwest

Reputation: 4450

Abstract Base Class requiring concretions to have minimum class attributes but allowing for extra

I have seen a few questions related to my issue but I've still been unable to come up with an adequate approach.

I have the following base and subclass:

class Base:
    __attrs__ = {'one', 'two', 'three'}

    def __init__(self, nums: {str} = None):
        if nums:
            self.__attrs__.update(nums)
        self.nums = self.__attrs__


class Child(Base):
    __attrs__ = {'four', 'five', 'six'}

    def __init__(self, nums: {str} = None):
        # self.__attrs__.update(super().__attrs__.copy())  <-----this works when uncommented
        super(Child, self).__init__(nums)


b = Base()
print(b.nums)

c = Child()
print(c.nums)

# The output, as expected.
{'three', 'two', 'one'}
{'four', 'five', 'six'}

The child class is obviously overriding the base class' __attrs__ value during instantiation. What I'm trying to figure out is how I can have the values from the base class' __attrs__ inherited and extended by the subclass—such that the output would be as follows:

{'three', 'two', 'one','four', 'five', 'six'}

But not necessarily (or likely) in that order. The first line of the subclass' __init__ copies the parents' attributes to the instance and achieves the end result. However, as I plan on having lots of concretions here, I'm trying to wrap my head around a way to do that in the base class somehow.

Upvotes: 0

Views: 270

Answers (2)

juanpa.arrivillaga
juanpa.arrivillaga

Reputation: 95948

This is a job for __init_subclass__ which is a classmethod you can define on a base class that will be called to initialize any subclasses.

>>> class Base:
...     attrs = {'one', 'two', 'three'}
...     def __init_subclass__(cls): # cls will be a subclass!
...         cls.attrs.update(super(cls, cls).attrs)
...
>>> class Child(Base):
...     attrs = {'four', 'five'}
...
>>> Child.attrs
{'two', 'four', 'three', 'five', 'one'}
>>> Base.attrs
{'two', 'one', 'three'}

Note, in this case, we pass cls explicitly as both arguments to super. Otherwise, if you use the zero-argument form, Foo would be passed as the first argument, and actually need the subclass! This is a special case, you would almost never want to do this in a regular class method (or instance method!).

Note, as usual, __init_subclass__ is there to help you avoid having to implement a metaclass. You could do this with a metaclass as follows, although it's a bit clunky because you can't assume the parent has an attrs.

>>> class AttrsMeta(type):
...     def __init__(self, *args, **kwargs):
...         try:
...             parent_attrs = super(self, self).attrs
...         except AttributeError: # parent no attrs, base class?
...             return # handle however you like
...         self.attrs.update(parent_attrs) # assume the class always has an attrs defined
...
>>> class Base(metaclass=AttrsMeta):
...     attrs = {'one', 'two', 'three'}
...
>>> class Child(Base):
...     attrs = {'four', 'five'}
...
>>> Child.attrs
{'two', 'four', 'three', 'five', 'one'}
>>> Base.attrs
{'two', 'one', 'three'}

Again, notice super(self, self)...

If all of this is a little too magical/implicit for your taste, and I might be inclined to agree, you could always define a decorator, I'd go with an API somewhat like this:

>>> def attrs(cls):
...     def update_attrs(subclass):
...         subclass.attrs.update(super(subclass, subclass).attrs)
...         return subclass
...     cls.update_attrs = update_attrs
...     return cls
...
>>> @attrs
... class Base:
...     attrs = {'one', 'two', 'three'}
...
>>> @Base.update_attrs
... class Child(Base):
...     attrs = {'four', 'five'}
...
>>> Child.attrs
{'two', 'four', 'three', 'five', 'one'}
>>> Base.attrs
{'two', 'one', 'three'}

Upvotes: 1

CyanoKobalamyne
CyanoKobalamyne

Reputation: 237

You can change your base class to add its own attributes:

class Base:
    __attrs__ = {'one', 'two', 'three'}

    def __init__(self, nums: {str} = None):
        self.__attrs__.update(Base.__attrs__)  # <-- add this.
        if nums:
            self.__attrs__.update(nums)
        self.nums = self.__attrs__

Upvotes: 1

Related Questions