Ignacio Vergara Kausel Ignacio Vergara Kausel - 6 months ago 21
Python Question

Reusable validating class attributes

I have a class which implements attributes through the

@property
decorator as follows

class num(object):

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

@property
def val(self):
return self._val

@val.setter
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."""
try:
if not isinstance(value, types):
obj._val = default
raise TypeError
if value < 0:
obj._val = default
raise ValueError
else:
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
val
. I would like to make the function
validation
more general, to be able to reuse for other attributes (for example an arbitrary index). I can see that my problem resides in the
obj._val
, 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
validation
to be aware of which attribute has to validate. Or perhaps is by using a completely different approach.

Thanks in advance!

Answer

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)
        else:
            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