N0ne
N0ne

Reputation: 53

Avoid class variable in python subclass when parent class requires to declare it

I read that it is considered bad practice to create a variable in the class namespace and then change its value in the class constructor.

(One of my sources: SoftwareEngineering SE: Is it a good practice to declare instance variables as None in a class in Python.)

Consider the following code:

# lib.py
class mixin:
    def __init_subclass__(cls, **kwargs):
        cls.check_mixin_subclass_validity(cls)
        super().__init_subclass__(**kwargs)

    def check_mixin_subclass_validity(subclass):
        assert hasattr(subclass, 'necessary_var'), \
            'Missing necessary_var'

    def method_used_by_subclass(self):
        return self.necessary_var * 3.14


# app.py
class my_subclass(mixin):
    necessary_var = None

    def __init__(self, some_value):
        self.necessary_var = some_value

    def run(self):
        # DO SOME STUFF
        self.necessary_var = self.method_used_by_subclass()
        # DO OTHER STUFF

To force its subclass to declare the variable necessary_var, the class mixin uses the metaclass subclass_validator.

And the only way I know to makes it work on app.py side, is to initialized necessary_var as a class variable.

I am missing something or is it the only way to do so?

Upvotes: 5

Views: 1995

Answers (1)

Olivier Melançon
Olivier Melançon

Reputation: 22324

Short answer

You should check that attributes and methods exist at instantiation of a class, not before. This is what the abc module does and it has good reasons to work like this.

Long answer

First, I would like to point out that it seems what you want to check is that an instance attribute exists.

Due to Python dynamic nature, it is not possible to do so before an instance is created, that is after the call to __init__. We could define Mixin.__init__, but we would then have to rely on the users of your API to have perfect hygiene and to always call super().__init__.

One option is thus to create a metaclass and add a check in its __call__ method.

class MetaMixin(type):
    def __call__(self, *args, **kwargs):
        instance = super().__call__(*args, **kwargs)
        assert hasattr(instance, 'necessary_var')

class Mixin(metaclass=MetaMixin):
    pass

class Foo(Mixin):
    def __init__(self):
        self.necessary_var = ...

Foo() # Works fine

class Bar(Mixin):
    pass

Bar() # AssertionError

To convince yourself that it is good practice to do this at instantiation, we can look toward the abc module which uses this behaviour.

from abc import abstractmethod, ABC

class AbstractMixin(ABC):
    @abstractmethod
    def foo(self):
        ...

class Foo(AbstractMixin):
    pass

# Right now, everything is still all good

Foo() # TypeError: Can't instantiate abstract class Foo with abstract methods foo

As you can see the TypeError was raise at instantiation of Foo() and not at class creation.

But why does it behave like this?

The reason for that is that not every class will be instantiated, consider the example where we want to inherit from Mixin to create a new mixin which checks for some more attributes.

class Mixin:
    def __init_subclass__(cls, **kwargs):
        assert hasattr(cls, 'necessary_var')
        super().__init_subclass__(**kwargs)

class MoreMixin(Mixin):
    def __init_subclass__(cls, **kwargs):
        assert hasattr(cls, 'other_necessary_var')
        super().__init_subclass__(**kwargs)

# AssertionError was raised at that point

class Foo(MoreMixin):
    necessary_var = ...
    other_necessary_var = ...

As you see, the AssertionError was raised at the creation of the MoreMixin class. This is clearly not the desired behaviour since the Foo class is actually correctly built and that is what our mixin was supposed to check.

In conclusion, the existence of some attribute or method should be done at instantiation, Otherwise, you are preventing a whole lot of helpful inheritance techniques. This is why the abc module does it like that and this is why we should.

Upvotes: 2

Related Questions