Search code examples
pythoninheritancemultiple-inheritancepython-descriptors

Intuative way to inherit validating classes


I've been using this style of inheritance to validate values set on instances of objects, but I'm wondering if there is a more fluent way to do this.

I'm following a spec where items of a certain classification (Foo) contain elements of a certain composition (Fe).

class Typed:
    def __set__(self, obj, value):
        assert isinstance(value, self._type), 'Incorrect type'


class Integer(Typed):
    _type = int


class Float(Typed):
    _type = float


class Positive(Typed):
    def __set__(self, obj, value):
        super().__set__(obj, value)
        assert value >= 0, 'Positive Values Only Accepted'


class PositiveInteger(Integer, Positive):
    pass


class PositiveFloat(Float, Positive):
    pass


class Sized(Typed):
    def __set__(self, obj, value):
        super().__set__(obj, value)
        assert value <=  2**self.size-1, f'{value} is too High'

class Fe(Sized, PositiveInteger):
    name = 'Integer, 8 bit unsigned'
    size = 8
    

class Foo(Fe):
    name = 'Classificaion1'
    def __set__(self, obj, id):
        super().__set__(obj, id)
        obj._id = id
    def __get__(self, obj, objType=None):
        return obj._id
    def __del__(self):
        pass

Solution

  • If you really need this level of abstraction, this is possibly the best way you can do it. My suggestion bellow can maybe save one line per class. If you can afford to have attributes like "size" and "type" to be defined on the final class, a richer base class and a declarative structure containing the checks as "lambda functions" can be used like this.

    Note the usage of __init_subclass__ to check if all the parametes needed for the guard expressions are defined:

    from typing import Sequence
    
    GUARDS = {
        "typed": ((lambda self, value: "Incorrect type" if not instance(value, self._type) else None), ("_typed",)),
        "positive": ((lambda self, value: "Only positive values" if value < 0 else None), ()),
        "sized": ((lambda self, value: None if value <= 2 ** self.size - 1 else f"{value} must be smaller than 2**{self.size}"), ("size",)),
    }
    
    class DescriptorBase:
        guards: Sequence[str]
    
        def __init_subclass__(cls):
            _sentinel = object()
            for guard_name in cls.guards:
                guard = GUARDS[guard_name]
                required_attrs = guard[1]
                missing = []
                for attr in required_attrs:
                    if getattr(cls, attr, _sentinel) is _sentinel:
                        missing.append(attr)
                if missing:
                        raise TypeError("Guarded descriptor {cls.__name__} did not declare required attrs: {missing}")
        
        def __set_name__(self, owner, name):
            self._name = f"_{name}""
    
        def __set__(self, instance, value):
            errors = []
            for guard_name in self.guards:
                if (error:= GUARDS[guard_name](self, value)) is not None:
                    errors.append(error)
            if errors:
                raise ValueError("\n".join(errors))
            setattr (instance, self._name, value)
        
        def __get__(self, instance, owner):
            if instance is None:
                return self
            return getattr(instance, self.name)
        
        def __del__(self, instance):
            delattr(instance, self._name)
    
    
    class Foo(DescriptorBase):
        guards = ("typed", "positive", "sized")
        size = 8
        type_ = int
        # No other code required here: __get__, __set__, __del__ handled in superclass
        
    class UseAttr:
        # Actual smart-attr usage:
        my_foo = Foo()
    

    Actually, if you want the class hierarchy, with less lines (no need to declare a __set__ method in each class), this approach can be used as well: just change __init_superclass__ to collect "guards" in all superclasses, and consolidate a single guards list on the class being defined, and then define your composable guard-classes just as:

    class Positive(BaseDescriptor):
        guards = ("positive",)
    
    class Sized(BaseDescriptor):
        guards = ("sized",)
        size = None
    
    class Foo(Positive, Sized):
        size = 8
    
    class Fe(Foo):
        name = "Fe name"
    

    Actually, the change needed for this to work can be as simple as:

        def __init_subclass__(cls):
            _sentinel = object()
            all_guards = []
            for supercls in cls.__mro__:
                all_guards.extend(getattr(supercls, "guards", ()))
            # filter unique:
            seem = {}
            new_guards = []
            for guard in all_guards:
                if guard not in seem:
                    new_guards.append(guard)
                seem.add(guard)
            cls.guards = new_guards
            for guard_name in cls.guards:
    

    Also note that you could also collect the contents of the "GUARDS" registry from each defined class, instead of having to declare everything as lambdas before hand. I think you can get the idea from here on.