Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Most Pythonic way to declare an abstract class property

Assume you're writing an abstract class and one or more of its non-abstract class methods require the concrete class to have a specific class attribute; e.g., if instances of each concrete class can be constructed by matching against a different regular expression, you might want to give your ABC the following:

@classmethod def parse(cls, s):     m = re.fullmatch(cls.PATTERN, s)     if not m:         raise ValueError(s)     return cls(**m.groupdict()) 

(Maybe this could be better implemented with a custom metaclass, but try to ignore that for the sake of the example.)

Now, because overriding of abstract methods & properties is checked at instance creation time, not subclass creation time, trying to use abc.abstractmethod to ensure concrete classes have PATTERN attributes won't work — but surely there should be something there to tell anyone looking at your code "I didn't forget to define PATTERN on the ABC; the concrete classes are supposed to define their own." The question is: Which something is the most Pythonic?

  1. Pile of decorators

    @property @abc.abstractmethod def PATTERN(self):     pass 

    (Assume Python 3.4 or higher, by the way.) This can be very misleading to readers, as it implies that PATTERN should be an instance property instead of a class attribute.

  2. Tower of decorators

    @property @classmethod @abc.abstractmethod def PATTERN(cls):     pass 

    This can be very confusing to readers, as @property and @classmethod normally can't be combined; they only work together here (for a given value of "work") because the method is ignored once it's overridden.

  3. Dummy value

    PATTERN = '' 

    If a concrete class fails to define its own PATTERN, parse will only accept empty input. This option isn't widely applicable, as not all use cases will have an appropriate dummy value.

  4. Error-inducing dummy value

    PATTERN = None 

    If a concrete class fails to define its own PATTERN, parse will raise an error, and the programmer gets what they deserve.

  5. Do nothing. Basically a more hardcore variant of #4. There can be a note in the ABC's docstring somewhere, but the ABC itself shouldn't have anything in the way of a PATTERN attribute.

  6. Other???

like image 687
jwodder Avatar asked Jul 21 '17 23:07

jwodder


People also ask

Are abstract classes Pythonic?

Python doesn't directly support abstract classes. But it does offer a module that allows you to define abstract classes.

How do we declare an abstract class?

You create an abstract class by declaring at least one pure virtual member function. That's a virtual function declared by using the pure specifier ( = 0 ) syntax. Classes derived from the abstract class must implement the pure virtual function or they, too, are abstract classes. // deriv_AbstractClasses.

How do you define an abstract property in Python?

To define an abstract method we use the @abstractmethod decorator of the abc module. It tells Python that the declared method is abstract and should be overridden in the child classes. We just need to put this decorator over any function we want to make abstract and the abc module takes care of the rest.

What decorator we use to declare abstract methods?

abc. abstractmethod(function) A decorator indicating abstract methods. Using this decorator requires that the class's metaclass is ABCMeta or is derived from it. A class that has a metaclass derived from ABCMeta cannot be instantiated unless all of its abstract methods and properties are overridden.


2 Answers

Python >= 3.6 Version

(Scroll down for a version that works for Python <= 3.5).

If you are fortunate enough to only be using Python 3.6 and not have to worry about backwards compatibility, you can use the new __init_subclass__ method which was introduced in Python 3.6 to make customizing class creation easier without resorting to metaclasses. When defining a new class, it is called as the last step before the class object is created.

In my opinion, the most pythonic way to use this would be to make a class decorator that accepts the attributes to make abstract, thus making it explicit to the user what they need to define.

from custom_decorators import abstract_class_attributes  @abstract_class_attributes('PATTERN') class PatternDefiningBase:     pass  class LegalPatternChild(PatternDefiningBase):     PATTERN = r'foo\s+bar'  class IllegalPatternChild(PatternDefiningBase):     pass 

The traceback might be as follows, and occurs at subclass creation time, not instantiation time.

NotImplementedError                       Traceback (most recent call last) ...      18     PATTERN = r'foo\s+bar'      19  ---> 20 class IllegalPatternChild(PatternDefiningBase):      21     pass  ...  <ipython-input-11-44089d753ec1> in __init_subclass__(cls, **kwargs)       9         if cls.PATTERN is NotImplemented:      10             # Choose your favorite exception. ---> 11             raise NotImplementedError('You forgot to define PATTERN!!!')      12       13     @classmethod  NotImplementedError: You forgot to define PATTERN!!! 

Before showing how the decorator is implemented, it is instructive to show how you could implement this without the decorator. The nice thing here is that if needed you could make your base class an abstract base class without having to do any work (just inherit from abc.ABC or make the metaclass abc.ABCMeta).

class PatternDefiningBase:     # Dear programmer: implement this in a subclass OR YOU'LL BE SORRY!     PATTERN = NotImplemented      def __init_subclass__(cls, **kwargs):         super().__init_subclass__(**kwargs)          # If the new class did not redefine PATTERN, fail *hard*.         if cls.PATTERN is NotImplemented:             # Choose your favorite exception.             raise NotImplementedError('You forgot to define PATTERN!!!')      @classmethod     def sample(cls):         print(cls.PATTERN)  class LegalPatternChild(PatternDefiningBase):     PATTERN = r'foo\s+bar' 

Here is how the decorator could be implemented.

# custom_decorators.py  def abstract_class_attributes(*names):     """Class decorator to add one or more abstract attribute."""      def _func(cls, *names):         """ Function that extends the __init_subclass__ method of a class."""          # Add each attribute to the class with the value of NotImplemented         for name in names:             setattr(cls, name, NotImplemented)          # Save the original __init_subclass__ implementation, then wrap         # it with our new implementation.         orig_init_subclass = cls.__init_subclass__          def new_init_subclass(cls, **kwargs):             """             New definition of __init_subclass__ that checks that             attributes are implemented.             """              # The default implementation of __init_subclass__ takes no             # positional arguments, but a custom implementation does.             # If the user has not reimplemented __init_subclass__ then             # the first signature will fail and we try the second.             try:                 orig_init_subclass(cls, **kwargs)             except TypeError:                 orig_init_subclass(**kwargs)              # Check that each attribute is defined.             for name in names:                 if getattr(cls, name, NotImplemented) is NotImplemented:                     raise NotImplementedError(f'You forgot to define {name}!!!')          # Bind this new function to the __init_subclass__.         # For reasons beyond the scope here, it we must manually         # declare it as a classmethod because it is not done automatically         # as it would be if declared in the standard way.         cls.__init_subclass__ = classmethod(new_init_subclass)          return cls      return lambda cls: _func(cls, *names) 

Python <= 3.5 Version

If you are not fortunate enough to only be using Python 3.6 and not have to worry about backwards compatibility, you will have to use a metaclass. Even though this is perfectly valid Python, one could debate how pythonic the solution is because metaclasses are hard to wrap your brain around, but I think it hits most of the points of The Zen of Python so I think it's not so bad.

class RequirePatternMeta(type):     """Metaclass that enforces child classes define PATTERN."""      def __init__(cls, name, bases, attrs):         # Skip the check if there are no parent classes,         # which allows base classes to not define PATTERN.         if not bases:             return         if attrs.get('PATTERN', NotImplemented) is NotImplemented:             # Choose your favorite exception.             raise NotImplementedError('You forgot to define PATTERN!!!')  class PatternDefiningBase(metaclass=RequirePatternMeta):     # Dear programmer: implement this in a subclass OR YOU'LL BE SORRY!     PATTERN = NotImplemented      @classmethod     def sample(cls):         print(cls.PATTERN)  class LegalPatternChild(PatternDefiningBase):     PATTERN = r'foo\s+bar'  class IllegalPatternChild(PatternDefiningBase):     pass 

This behaves exactly like the Python >= 3.6 __init_subclass__ method shown above (except the traceback will look bit different because it is routed through a different set of methods before failing).

Unlike the __init_subclass__ method, if you want to make a subclass an abstract base class you will have to do just a bit of extra work (you'll have to compose the metaclass with ABCMeta).

from abs import ABCMeta, abstractmethod  ABCRequirePatternMeta = type('ABCRequirePatternMeta', (ABCMeta, RequirePatternMeta), {})  class PatternDefiningBase(metaclass=ABCRequirePatternMeta):     # Dear programmer: implement this in a subclass OR YOU'LL BE SORRY!     PATTERN = NotImplemented      @classmethod     def sample(cls):         print(cls.PATTERN)      @abstractmethod     def abstract(self):         return 6  class LegalPatternChild(PatternDefiningBase):     PATTERN = r'foo\s+bar'      def abstract(self):         return 5  class IllegalPatternChild1(PatternDefiningBase):     PATTERN = r'foo\s+bar'  print(LegalPatternChild().abstract()) print(IllegalPatternChild1().abstract())  class IllegalPatternChild2(PatternDefiningBase):     pass 

Outputs just as you'd expect.

5 TypeError: Can't instantiate abstract class IllegalPatternChild1 with abstract methods abstract # Then the NotImplementedError if it kept on going. 
like image 132
SethMMorton Avatar answered Sep 26 '22 04:09

SethMMorton


I've been searching for something like this for quite a while, until yesterday I decided to dive into it. I like @SethMMorton's reply a lot, however 2 things are missing: allow a an abstract class to have a subclass that is abstract itself, and play nice with typehints and static typing tools such as mypy (which makes sense, since back in 2017 these were hardly a thing).

I started to set out to write a reply here with my own solution, however I realised I needed lots of tests and documentation, so I made it a proper python module: abstractcp.

Use (as of version 0.9.5):

class Parser(acp.Abstract):     PATTERN: str = acp.abstract_class_property(str)      @classmethod     def parse(cls, s):         m = re.fullmatch(cls.PATTERN, s)         if not m:             raise ValueError(s)         return cls(**m.groupdict())  class FooBarParser(Parser):     PATTERN = r"foo\s+bar"      def __init__(...): ...  class SpamParser(Parser):     PATTERN = r"(spam)+eggs"      def __init__(...): ... 

See for full use the page on pypi or github.

like image 33
Claude Avatar answered Sep 25 '22 04:09

Claude