Reputation: 985
I have an abstract class from which subclasses will derive. The concrete implementations should include within them an Enum class which holds a set of named constants.
from enum import Enum
class AbstractClass:
def run(self)
print('the values are:')
for enum in ClassEnum:
print(enum.value)
self.speak()
def speak(self):
raise NotImplementedError
class ConcreteClassFirst(AbstractClass):
class ClassEnum(Enum):
RED = 0
GREEN = 1
BLUE = 2
def speak(self)
print('the colors are:')
for enum in ClassEnum:
print(enum.name)
class ConcreteClassSecond(AbstractClass):
class ClassEnum(Enum):
LION = 'scary'
ELEPHANT = 'big'
GIRAFFE = 'tall'
def speak(self)
print('the animals are:')
for enum in ClassEnum:
print(enum.name)
this code in fact gives the correct behaviour, however I would like there to be some sort of notation (similar to the NotImplementedError
on the abstract speak()
method) That indicates the author of the concrete class should define an inner Enum
class called ClassEnum
. It is needed in fact for the run()
method.
Some ideas are to have something like
class AbstractClass:
class ClassEnum(Enum):
pass
def run(self):
...
but this won't raise an error if the subclass doesn't define its own version of ClassEnum
. We could try
class AbstractClass:
class ClassEnum(Enum):
raise NotImplementedError
def run(self):
...
But this predictably raises an error as soon as the AbstractClass
is defined
I could try
class AbstractClass:
@property
def ClassEnum(self):
raise NotImplementedError
def run(self):
...
But here it's not clear that in the subclass ClassEnum should in fact be a class. Perhpas this approach with some documentation could be appropriate..
Upvotes: 5
Views: 520
Reputation: 101
I think your last approach of using @property
is perhaps the best method.
But here it's not clear that in the subclass ClassEnum should in fact be a class. Perhpas this approach with some documentation could be appropriate..
This could be further specified by typing:
from typing import Type, override, List
from enum import Enum
from abc import ABC, abstractmethod
class AbstractClass(ABC):
@property
@abstractmethod
def ClassEnum(self) -> Type[Enum]:
pass
def iterate_enum(self):
for enum in self.ClassEnum:
print(enum)
class ConcreteClassFirst(AbstractClass):
@override
class ClassEnum(Enum):
FIRST = 1
SECOND = 2
class ConcreteClassSecond(AbstractClass):
@property
@override
def ClassEnum(self) -> List[int]:
return [1, 2]
cc1 = ConcreteClassFirst()
cc1.iterate_enum()
cc2 = ConcreteClassSecond()
cc2.iterate_enum()
Of course, no error will be thrown if the concrete class defines a function instead of a class, as the code can be run perfectly. But some static analysis tools like mypy will be able to catch the error:
mypy .\temp.py
temp.py:27: error: Signature of "ClassEnum" incompatible with supertype "AbstractClass" [override]
temp.py:27: note: Superclass:
temp.py:27: note: type[Enum]
temp.py:27: note: Subclass:
temp.py:27: note: list[int]
Found 1 error in 1 file (checked 1 source file)
If I delete ConcreteClassSecond
, then the program can pass mypy.
Alternatively, if runtime checking is wanted, then defining a __init_subclass__
in the abstract class can achieve this:
from typing import Type, Any, Optional
import functools
def require_inner_class(attr_name: str, subclass_of: Optional[Type[Any]] = None):
"""Decorator to enforce that a subclass has an attribute and optionally ensures it is a subclass of a given type."""
def decorator(cls):
orig_init_subclass = getattr(cls, "__init_subclass__", None)
@functools.wraps(cls.__init_subclass__)
def new_init_subclass(sub_cls, **kwargs):
if not hasattr(sub_cls, attr_name):
raise NotImplementedError(
f"Class {sub_cls.__name__} must implement {attr_name}")
attr_value = getattr(sub_cls, attr_name)
if subclass_of and not isinstance(attr_value, type):
raise TypeError(
f"Attribute {attr_name} in {sub_cls.__name__} must be a class, but got {type(attr_value).__name__}")
if subclass_of and not issubclass(attr_value, subclass_of):
raise TypeError(
f"Attribute {attr_name} in {sub_cls.__name__} must be a subclass of {subclass_of.__name__}")
# Call the original __init_subclass__ if it exists
if orig_init_subclass:
orig_init_subclass(**kwargs)
cls.__init_subclass__ = classmethod(new_init_subclass)
return cls
return decorator
Upvotes: 0
Reputation: 46
I use mixin like this. This mixin recursively checks the attribute definitions for your class and its parents. If you want disable check fore specific class you can add abc.ABC as base to сlass declaration or add parameter required_attrs_base with True value
from __future__ import annotations
import inspect
from typing import ClassVar
from enum import Enum
class RequiredAttrsMixin:
"""Add required attrs
Examples:
``` py linenums="1" title="Example"
class Foo(RequiredAttrsMixin):
required_attrs = ("model", "prefetch_class", "m2m_manager")
```
"""
required_attrs: ClassVar[tuple] = ()
"""Required attrs"""
required_attrs_check_abstract: ClassVar[bool] = False
"""Required abstract attrs"""
def __init_subclass__(cls, required_attrs_base=False, **kwargs):
super().__init_subclass__(**kwargs)
if not required_attrs_base:
cls.check_required_attrs()
@classmethod
def check_required_attrs(cls):
"""Check required attrs"""
if not cls.required_attrs_check_abstract and inspect.isabstract(cls):
return
for _cls in reversed(cls.__mro__):
if not hasattr(_cls, "required_attrs"):
continue
for attr in _cls.required_attrs:
if not hasattr(cls, attr):
raise AttributeError(f"Attr {attr} required dor class {cls.__name__}")
For your example
class AbstractClass(RequiredAttrsMixin, required_attrs_base=True):
required_attrs: ClassVar[tuple] = ("behavior_enum", "speak_text")
behavior_enum: ClassVar[type[Enum]]
speak_text: ClassVar[str]
def run(self):
print('the values are:')
for enum in self.behavior_enum:
print(enum.value)
self.speak()
def speak(self):
print(self.speak_text)
for enum in self.behavior_enum:
print(enum.name)
class ConcreteClassFirst(AbstractClass):
class behavior_enum(Enum):
RED = 0
GREEN = 1
BLUE = 2
speak_text = 'the colors are:'
class ConcreteClassSecond(AbstractClass):
class behavior_enum(Enum):
LION = 'scary'
ELEPHANT = 'big'
GIRAFFE = 'tall'
speak_text = 'the animals are:'
Upvotes: 0
Reputation: 985
I don't think there's a clean way in python short of strange metaclasses to enforce definition of class attributes on subclasses.
Given that, I think the next best thing is to instead use instance attributes passed through super.__init__()
.
from enum import Enum
class AbstractClass:
def __init__(self, behavior_enum):
"""
:param behavior_enum: enum class with values and names that determine class behavior
"""
self.behavior_enum = behavior_enum
def run(self)
print('the values are:')
for enum in self.behavior_enum:
print(enum.value)
self.speak()
def speak(self):
raise NotImplementedError
class ConcreteClassFirst(AbstractClass):
class behavior_enum(Enum):
RED = 0
GREEN = 1
BLUE = 2
def __init__(self)
super().__init__(self.behavior_enum)
def speak(self)
print('the colors are:')
for enum in self.behavior_enum:
print(enum.name)
class ConcreteClassSecond(AbstractClass):
class behavior_enum(Enum):
LION = 'scary'
ELEPHANT = 'big'
GIRAFFE = 'tall'
def __init__(self)
super().__init__(self.behavior_enum)
def speak(self)
print('the animals are:')
for enum in self.behavior_enum:
print(enum.name)
Upvotes: 1