Most Pythonic way to declare an abstract class property

2024/11/19 15:13:04

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???

Answer

You can use the __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:passclass 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     @classmethodNotImplementedError: 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 = NotImplementeddef __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!!!')@classmethoddef sample(cls):print(cls.PATTERN)class LegalPatternChild(PatternDefiningBase):PATTERN = r'foo\s+bar'

Here is how the decorator could be implemented.

# custom_decorators.pydef 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 NotImplementedfor 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 thatattributes 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 clsreturn lambda cls: _func(cls, *names)
https://en.xdnf.cn/q/26428.html

Related Q&A

PyQt on Android

Im working on PyQt now, and I have to create the application on Android, Ive seen the kivy library, but its too crude.Is there any way now to run an application on Android made on PyQt?

How to elementwise-multiply a scipy.sparse matrix by a broadcasted dense 1d array?

Suppose I have a 2d sparse array. In my real usecase both the number of rows and columns are much bigger (say 20000 and 50000) hence it cannot fit in memory when a dense representation is used:>>…

Do I have to do StringIO.close()?

Some code:import cStringIOdef f():buffer = cStringIO.StringIO()buffer.write(something)return buffer.getvalue()The documentation says:StringIO.close(): Free the memory buffer. Attempting to do furtherop…

Python: ulimit and nice for subprocess.call / subprocess.Popen?

I need to limit the amount of time and cpu taken by external command line apps I spawn from a python process using subprocess.call , mainly because sometimes the spawned process gets stuck and pins the…

array.shape() giving error tuple not callable

I have a 2D numpy array called results, which contains its own array of data, and I want to go into it and use each list: for r in results:print "r:"print ry_pred = np.array(r)print y_pred.sh…

Python unittest - Ran 0 tests in 0.000s

So I want to do this code Kata for practice. I want to implement the kata with tdd in separate files:The algorithm:# stringcalculator.py def Add(string):return 1and the tests:# stringcalculator.spec.…

How and where does py.test find fixtures

Where and how does py.test look for fixtures? I have the same code in 2 files in the same folder. When I delete conftest.py, cmdopt cannot be found running test_conf.py (also in same fo…

Python match a string with regex [duplicate]

This question already has answers here:What exactly do "u" and "r" string prefixes do, and what are raw string literals?(7 answers)What exactly is a "raw string regex" an…

What are the consequences of disabling gossip, mingle and heartbeat for celery workers?

What are the implications of disabling gossip, mingle, and heartbeat on my celery workers?In order to reduce the number of messages sent to CloudAMQP to stay within the free plan, I decided to follow …

How to determine if an exception was raised once youre in the finally block?

Is it possible to tell if there was an exception once youre in the finally clause? Something like:try:funky code finally:if ???:print(the funky code raised)Im looking to make something like this m…