1

Essentially what I want is to require that subclasses of an abstract base classes not only implement certain attributes or methods, but can also make requirements on those, such as data types or allowed values.

For example, let's say I want to require classes that have a name and that that name starts with the letter 'a':

from abc import ABC, abstractproperty

class Base(ABC):
     @abstractproperty
     def name(self):
         assert self.name[0] == 'a' # or similar; help needed here

class Derived1(Base):
    name = 'albert' # I want this class definition to work

class Derived2(Base):
    name = 'john' # I want this class definition to fail the 'a' assertion

Where/how would I assert this in the base class?

martineau
  • 119,623
  • 25
  • 170
  • 301
user3059201
  • 775
  • 2
  • 7
  • 11
  • Who set the name property? Is it it the class user when he create an instance? Can you show an example of usage of those classes? – balderman Jun 20 '19 at 18:42
  • the name property is set at Derived definition time. we don't know ahead of time how many Derived class definitions we'll have, but we know in advance that they have names that must conform to certain requirements. not sure what else you'd like to know – user3059201 Jun 20 '19 at 18:51
  • This is not only ridiculously unpythonic, but it would be a poor design choice in a statically typed language as well. If you really want the property `name` to start with `'a'` for all baseclasses... _then do that_. You don't need to enforce it: convention over configuration. In Python, you really _can't_ enforce it (consider monkey patching). Another thing you could do is make a function that prepends `'a'` to all `name`s if they don't have it – Matt Messersmith Jun 20 '19 at 18:54
  • this is just an example of a simple check. let's say instead that we wanted to check that `name` is type string. or that name is in the set `{'jack', 'jill'}`. could be any sort of data integrity check – user3059201 Jun 20 '19 at 18:56
  • The strange thing here for me is that the `name` value is set on design time and there is a design time rule in the base class that should assert if the name is not legal. I cant understand what this is good for.. – balderman Jun 20 '19 at 18:59
  • this is good for the case of multiple people adding code and wanting to check that each new class definition conforms to certain rules. is there a better way to accomplish that? also, the code i've written above does not accomplish the desired effect – user3059201 Jun 20 '19 at 19:02
  • If you want `name` to be a string then make it a string. Or if `name` should be in `{'jack', 'jill'}`, then do that. Why do you think an `ABC` is what you need to use? Or inheritance at all? You can't _enforce_ static things like this in Python at "compile time". It's not a statically typed language. If you _really want to check at runtime_ (which you don't need to do if you just have a policy of convention over configuration) write a function to check it, and fail if you get a bad value – Matt Messersmith Jun 20 '19 at 19:04
  • @user3059201, don't worry, wait for a minute – RomanPerekhrest Jun 20 '19 at 19:06
  • 2
    Setting a class attribute does not define a property. – chepner Jun 20 '19 at 19:25
  • I think you're confusing _class_ attributes and _instance_ attributes. `properties` are generally used to define and control instance attributes, but writing something like `name = 'albert'` in class `Derived1` is defining a class attribute which isn't the same thing. – martineau Jun 20 '19 at 19:26
  • If you want to control or check class construction, consider using a metaclass — that's what they do. – martineau Jun 20 '19 at 19:28
  • @martineau `__init_subclass__` would be sufficient here. – chepner Jun 20 '19 at 19:31
  • @chepner: Good point...I was forgetting about that [addition](https://docs.python.org/3/reference/datamodel.html#customizing-class-creation) to Python 3.6 which would make a metaclass unnecessary. – martineau Jun 20 '19 at 19:39

1 Answers1

4

Use __init_subclass__ to enforce restrictions on class attributes.

class Base:
    def __init_subclass__(cls):
        try:
            name = cls.name
        except AttributeError:
            raise ValueError("No name attribute")

        if name[0] != "a":
            raise ValueError("first letter of name is not 'a'")


class Derived1(Base):
    name = 'albert' # OK

class Derived2(Base):
    name = 'john' # Fails due to first letter 'j'

# Fails because Derived3.name is never defined
class Derived3(Base):
    pass  
chepner
  • 497,756
  • 71
  • 530
  • 681
  • This is p cool, +1, but this feels like a workaround for a poor design decision, and probably some serious overkill. If we want all our `name` properties to start with `'a'`...then we do that and be done with it, good riddance! Just like if we want `name` to be a string...then make it a damn string and move on (we do this _all the time_ by not checking types and assuming they're okay). Do we really need a construct to enforce it? And what's wrong with a _simple_ runtime check? Stuff like this just doesn't feel Pythonic. Plus inheritance isn't meant for this kind of shenanigans...I digress – Matt Messersmith Jun 20 '19 at 19:57
  • 1
    Names starting with 'a' was the example for the question. There's nothing wrong with defining a base class to enforce an interface that will be implemented by subclasses. – chepner Jun 20 '19 at 20:03