17

What I'd really like to do is something like this:

class X(metaclass=abc.ABCMeta):
    @abc.abstractAttribute # this doesn't exist
    var = [1,2]

class Y(X):
    var = X.var + [3,4]

This would force any subclasses of X to implement a static var attribute.

But there's no way to define a static attribute as abstract.

Attempting to use @abc.abstractmethod, combined with property, I can get close:

class X(metaclass=abc.ABCMeta):
    @property
    @abc.abstractmethod
    def var(self):
        return [1,2]

class Y(X):
    @property
    def var(self):
        return super().var + [3,4]

y=Y(); y.var gives [1,2,3,4] as desired.

But the goal is to create a static attribute/property, not an instance attribute/property.

When creating as a @staticmethod:

class X(metaclass=abc.ABCMeta):
    @property
    @staticmethod
    @abc.abstractmethod
    def var():
        return [1,2]

class Y(X):
    @property
    @staticmethod
    def var():
        return X.var + [3,4]

... then y=Y(); y.var gives TypeError 'staticmethod' object is not callable

If I switch the decorator order, this will resolve the staticmethod callable error:

class X(metaclass=abc.ABCMeta):
    @staticmethod
    @property
    @abc.abstractmethod
    def var():
        return [1,2]

class Y(X):
    @staticmethod
    @property
    def var():
        return X.var + [3,4]

The static method will work, but the property will not function as intended: y=Y(); y.var returns the property itself, <property at 0x2c16aa1b3b8>.

This answer is close to what's desired, but it doesn't work in this case since the base class, X, will already have a var attribute (so subclass can access via super).

How do you define a python class to have an abstract, static attribute/property?

sam-6174
  • 3,104
  • 1
  • 33
  • 34

4 Answers4

6

If the requirement that it be a property is flexible, you could just define a static abstract method:

class X:
  @staticmethod
  @abstractmethod
  def var():
    pass

class Y(X):
  @staticmethod
  def var():
    return [1, 2]

>>> Y.var()
[1, 2]
Ben Caine
  • 1,128
  • 3
  • 15
  • 25
4

Maybe a variation on your solution could work - instead of using dict simply testing whether parent's and subclass static variables are the same object. It works for grandchildren and slots, but the tradeoff is that it will throw the exception if you override None with None, or other object with the same object, so it's not perfect.

class X:
    var = [1,2]

    def __init_subclass__(cls):
        if X.var is cls.var:
            raise NotImplementedError(
                "Attribute '{}' has not been overriden in class '{}'" \
                .format('var', cls.__name__)
            )

Maybe I'm overlooking something crucial, but it worked in simple use cases.

TrakJohnson
  • 1,755
  • 2
  • 18
  • 31
Speckhouse
  • 41
  • 2
2

This builds upon the approach of other answers that use __init_subclass__. It searches for the attribute in every base class (via its MRO) except X itself. This solves the issue of grandchildren which do not explicitly overwrite the attribute.

class X(metaclass=abc.ABCMeta):
    var = [1,2]

    def __init_subclass__(cls):
        if not any("var" in base.__dict__ for base in cls.__mro__ if base is not X):
            raise NotImplementedError(
                f"Attribute 'var' has not been overwritten in class '{cls.__name__}'"
            )

It does not address __slots__. However, defining __slots__ in your classes isn't a problem if you stick to using a class attribute to overwrite the abstract attribute.

bgfvdu3w
  • 1,548
  • 1
  • 12
  • 17
1

Let me know if you're aware of a more traditional way to solve this that will also work for classes that use __slots__. But for my use case, the below will work:

class X:
    var = [1,2]

    def __init_subclass__(cls):
        attr_name = 'var'
        if not attr_name in cls.__dict__:
            raise NotImplementedError(
                "Attribute '{}' has not been overriden in class '{}'" \
                .format(attr_name, cls.__name__)
            )

class Y(X):
    # var = X.var + [3,4] # this will work
    pass                  # this won't work

y = Y()

edit: There is another downside to using this approach, which is that this will wrongly error for grandchildren who do not override:

class Y(X):
    var = X.var + [3,4]

y = Y() # this works

class Z(Y):
    pass

z = Z() # this errors

I will leave this answer as unmarked for someone to suggest a better alternative.

sam-6174
  • 3,104
  • 1
  • 33
  • 34