Ignacio Vergara Kausel Ignacio Vergara Kausel - 4 months ago 11x
Python Question

Reusable validating class attributes

I have a class which implements attributes through the

decorator as follows

class num(object):

def __init__(self, value):
self.val = value

def val(self):
return self._val

def val(self, value):
validation(self, (int, float), value, 0.0)

When the value of the attribute is to be set, it should get validated.

def validation(obj, types, value, default):
"""Checks if the input provided for the attribute is valid."""
if not isinstance(value, types):
obj._val = default
raise TypeError
if value < 0:
obj._val = default
raise ValueError
obj._val = float(value)
except ValueError as e:
print("Should be a positive number. Value set at ", default)
except TypeError:
print("Should be a number. Value set at ", default)

This code works for this particular case, where I have just one a priori known attribute
. I would like to make the function
more general, to be able to reuse for other attributes (for example an arbitrary index). I can see that my problem resides in the
, where I'm breaking encapsulation.

I guess that one possible solution is through the use of a method decorator chained to the setter of the attribute or maybe passing an extra argument to
to be aware of which attribute has to validate. Or perhaps is by using a completely different approach.

Thanks in advance!


I would say one of the cleanest/reusable approaches to achieve this in python is with descriptors. The java-style get/set methods is just too verbose for this kind of requirement.

Here's what your model would look like - in just 3 lines:

class Person(object):
    balance = Integer('_balance')
    age = PositiveInteger('_age')

A few scenarios:

person = Person()

person.balance = 0  # OK
person.balance = '0'  # OK
person.balance = 'wrong'  # TypeError

person.age = 30  # OK
person.age = '30'  # OK
person.age = -1  # ValueError
person.age = 'wrong'  # TypeError

Implementation of descriptors Integer and PositiveInteger:

class Integer(object):
    def __init__(self, name, default=None):
        self.name = name
        self.default = default

    def __get__(self, instance, owner):
        return getattr(instance, self.name, self.default)

    def clean(self, value):
        if isinstance(value, int) or str(value).isdigit():
            return int(value)
        return value

    def __set__(self, instance, value):
        if isinstance(self.clean(value), int):
            setattr(instance, self.name, value)
            raise TypeError('`{}` not a valid integer'.format(value))

class PositiveInteger(Integer):
    def clean(self, value):
        x = super(PositiveInteger, self).clean(value)
        if isinstance(x, int) and x < 0:
            raise ValueError('`{}` is not a positive integer'.format(value))
        return x