5

I have spent a lot of time researching this, but none of the answers seem to work how I would like.

I have an abstract class with a class attribute I want each subclass to be forced to implement

class AbstractFoo():
    forceThis = 0

So that when I do this

class RealFoo(AbstractFoo):
    pass

it throws an error telling me it can't create the class until I implement forceThis.

How can I do that?

(I don't want the attribute to be read-only, but if that's the only solution, I'll accept it.)

For a class method, I've discovered I can do

from abc import ABCMeta, abstractmethod

class AbstractFoo(metaclass=ABCMeta):
    @classmethod
    @abstractmethod
    def forceThis():
        """This must be implemented"""

so that

class RealFoo(AbstractFoo):
    pass

at least throws the error TypeError: Can't instantiate abstract class EZ with abstract methods forceThis

(Although it doesn't force forceThis to be a class method.)

How can I get a similar error to pop up for the class attribute?

Pro Q
  • 4,391
  • 4
  • 43
  • 92
  • 2
    You can use `abstractproperty`, also from the abc module. Note that this requires the attribute to be a property, for reasons discussed a bit [here](http://stackoverflow.com/questions/23831510/python-abstract-attribute-not-property). – BrenBarn May 04 '17 at 18:35
  • [this](http://stackoverflow.com/a/32536493/5049813) which I found in your link @BrenBarn is very close to what I want, but the error only comes up when you try to access the property, not when the class is defined. – Pro Q May 04 '17 at 23:33
  • 2
    It will come up if you try to instantiate the class. Do you really need it to happen when you define the class? If so, you could do something along the lines suggested [here](http://stackoverflow.com/questions/30876344/can-i-prevent-class-definition-unless-a-method-is-implemented). – BrenBarn May 05 '17 at 05:18
  • Another way to get class-definition-time errors would be by using a class decorator to do the checking, although unlike metaclasses, they're not inheritable. One nice aspect of using them is that, unlike with metaclasses, the syntax for them is unchanged between Python 2 and 3. – martineau May 06 '17 at 10:15

3 Answers3

4

You can do this by defining your own metaclass. Something like:

 class ForceMeta(type):
     required = ['foo', 'bar']

     def __new__(mcls, name, bases, namespace):
         cls = super().__new__(mcls, name, bases, namespace)
         for prop in mcls.required:
            if not hasattr(cls, prop):
               raise NotImplementedError('must define {}'.format(prop))
         return cls

Now you can use this as the metaclass of your own classes:

class RequiredClass(metaclass=ForceMeta):
     foo = 1

which will raise the error 'must define bar'.

Daniel Roseman
  • 588,541
  • 66
  • 880
  • 895
  • The question was specifically about how to create an abstract **property**, whereas this seems like it just checks for the existence of any sort of a class attribute. – martineau May 04 '17 at 19:07
  • @martineau what's the difference between a class property and a class attribute? (I didn't know there was a difference - I may need to reword my question to help clarify.) – Pro Q May 04 '17 at 23:19
  • 2
    @martineau After reading a bit more, I think I'm talking about attributes. I have edited the question to reflect this. Thanks for bringing this issue to my attention! – Pro Q May 04 '17 at 23:37
  • @DanielRoseman do you know of any way to do it where I don't have to put it as a metaclass? I would like to just use normal classes if possible. – Pro Q May 04 '17 at 23:38
  • 1
    @ProQ The only way to change how a class is defined programmatically is to use a meta class. You used one yourself: ABCMeta. – zondo May 04 '17 at 23:41
  • @zondo True... Thank you! – Pro Q May 04 '17 at 23:43
  • @zondo: That's not the only way, there's also class decorators—although I'm unsure if they would help here or not. – martineau May 04 '17 at 23:43
  • @zondo I guess my issue is that I'm afraid I won't remember to put it as a metaclass for the non-abstract classes that use it. The beauty of ABCMeta is that I only have to use it on the abstract class. – Pro Q May 04 '17 at 23:45
  • 1
    @ProQ: A sub-class will inherit a meta class from its parent. I wrote something very similar to your requirement just a few weeks ago. It takes its list of required attributes from an attribute of the base class. All of the sub-classes of that would then be checked. https://gitlab.com/snippets/1661195 – zondo May 05 '17 at 00:34
  • @ProQ: Well, one important difference between a class property and a class attribute is that properties typically manage a private attribute, which though them can then effectively be made read-only—how important is that? – martineau May 05 '17 at 14:45
  • @martineau I really dislike having to create two things for one property. Like, really really really dislike. – Pro Q May 06 '17 at 02:11
  • @ProQ: OK, just be aware that read-only properties have other uses, for example to simulate a "virtual" attribute which the class doesn't actually have. As a trivial example, consider a hypothetical `Rectangle` class with `height` and `width` attributes, could also be given a property for `area`, which is computed on-the-fly from the two real attributes as needed—but doesn't support being set via direct assignment. – martineau May 06 '17 at 09:49
2

I came up with a solution based on those posted earlier. (Thank you @Daniel Roseman and @martineau)

I created a metaclass called ABCAMeta (the last 'A' stands for 'Attributes').

The class has two ways of working.

  1. A class which just uses ABCAMeta as a metaclass must have a property called required_attributes which should contain a list of the names of all the attributes you want to require on future subclasses of that class

  2. A class whose parent's metaclass is ABCAMeta must have all the required attributes specified by its parent class(es).

For example:

class AbstractFoo(metaclass=ABCAMeta):
    required_attributes = ['force_this']

class RealFoo(AbstractFoo):
    pass

will throw an error:

NameError: Class 'RealFoo' has not implemented the following attributes: 'force_this'

Exactly how I wanted.

from abc import ABCMeta

class NoRequirements(RuntimeError):
        def __init__(self, message):
            RuntimeError.__init__(self, message)

class ABCAMeta(ABCMeta):
    def __init__(mcls, name, bases, namespace):
        ABCMeta.__init__(mcls, name, bases, namespace)

    def __new__(mcls, name, bases, namespace):
        def get_requirements(c):
            """c is a class that should have a 'required_attributes' attribute
            this function will get that list of required attributes or
            raise a NoRequirements error if it doesn't find one.
            """

            if hasattr(c, 'required_attributes'):
                return c.required_attributes
            else:
                raise NoRequirements(f"Class '{c.__name__}' has no 'required_attributes' property")

        cls = super().__new__(mcls, name, bases, namespace)
        # true if no parents of the class being created have ABCAMeta as their metaclass
        basic_metaclass = True
        # list of attributes the class being created must implement
        # should stay empty if basic_metaclass stays True
        reqs = []
        for parent in bases:
            parent_meta = type(parent)
            if parent_meta==ABCAMeta:
                # the class being created has a parent whose metaclass is ABCAMeta
                # the class being created must contain the requirements of the parent class
                basic_metaclass=False
                try:
                    reqs.extend(get_requirements(parent))
                except NoRequirements:
                    raise
        # will force subclasses of the created class to define
        # the attributes listed in the required_attributes attribute of the created class
        if basic_metaclass:
            get_requirements(cls) # just want it to raise an error if it doesn't have the attributes
        else:
            missingreqs = []
            for req in reqs:
                if not hasattr(cls, req):
                    missingreqs.append(req)
            if len(missingreqs)!=0:
                raise NameError(f"Class '{cls.__name__}' has not implemented the following attributes: {str(missingreqs)[1:-1]}")
        return cls

Any suggestions for improvement are welcome in the comments.

Pro Q
  • 4,391
  • 4
  • 43
  • 92
0

Although you can do something very similar with a metaclass, as illustrated in @Daniel Roseman's answer, it can also be done with a class decorator. A couple of advantages they have are that errors will occur when the class is defined, instead of when an instance of one is created, and the syntax for specifying them is the same in both Python 2 and 3. Some folks also find them simpler and easier to understand.

def check_reqs(cls):
    requirements = 'must_have',

    missing = [req for req in requirements if not hasattr(cls, req)]
    if missing:
        raise NotImplementedError(
            'class {} did not define required attribute{} named {}'.format(
                cls.__name__, 's' if len(missing) > 1 else '',
                ', '.join('"{}"'.format(name) for name in missing)))
    return cls

@check_reqs
class Foo(object):  # OK
    must_have = 42

@check_reqs
class Bar(object):  # raises a NotImplementedError
    pass
Community
  • 1
  • 1
martineau
  • 119,623
  • 25
  • 170
  • 301
  • Interesting! I like this approach. I think I may still be able to come up with one that may suit me better though. Remembering to type that "@check_reqs" is going to be my main issue. – Pro Q May 07 '17 at 03:28
  • @ProQ: Having to remember the `@check_reqs` isn't any more difficult to recall than specifying a `metaclass=Something` when defining a `class`. – martineau May 07 '17 at 05:46
  • but notice that when creating RealFoo in my example in the question, I don't have to set the metaclass. It's for stuff like RealFoo where I don't want to remember to have to do stuff. I just want to subclass something and then have python remind me I forgot to override/put in those attributes, just like it does for methods. – Pro Q May 08 '17 at 02:15