9

I want to create a Pojo like class for User in Python. Each of the property will involve some validations. For example: I don't want someone to create a User with a negative value of age. To acheive that I will end up writing a class something like below.

 class User:
        def __init__(self, age):
            self.age = age

        @property
        def age(self):
            return self._age

        @age.setter
        def age(self, value):
            print("Called")
            if value >= 0:
                self._age = 0
            else:
                raise ValueError("Age can't be negative")

I am bit scared after seeing the class above. Reasons:

  1. My User class will have somewhere around 50'ish properties. I am bit scared to imagine how big this class will look in that case.

Is this a correct way of creating such classes in Python?

ThinkGeek
  • 4,749
  • 13
  • 44
  • 91
  • 3
    1. `self.age = age` is sufficient in the constructor. – Klaus D. Oct 21 '18 at 05:37
  • You mean self._age? – ThinkGeek Oct 21 '18 at 05:40
  • Maybe you could use the absolute value function `abs()` when setting the user's age? – Red Cricket Oct 21 '18 at 05:40
  • The thing to do would be to define your own positive integer type but defining new types in Python is not simple. – Red Cricket Oct 21 '18 at 05:47
  • 1
    If you **really** need to validate 50 fields in your class, I suggest you to think about some DSL-like approach for this task. One way is to declare these fields as a list like `[('age', 'int', lambda x: x >= 0), ...]` where first element is field name, second is type and third is a validator, and then correctly handle `__getattr__`, comparators, etc. in your class. Another way is to declare fields like Django models, but it might be harder to implement. You will reduce your (repeated) code dramatically, though you might loose completion support provided by IDE for your properties. – awesoon Oct 21 '18 at 05:53
  • @soon What is DSL in this context? can you please ellaborate this with an example maybe as an answer to the question? – ThinkGeek Oct 21 '18 at 06:28
  • Take a look at `attrs` .... – Julian Camilleri Oct 21 '18 at 06:31
  • *"You mean self._age?"* – No, he means `self.age = age` to trigger your property setter which already contains the validation code. – deceze Oct 21 '18 at 06:31
  • ok I got that, but still code will look too clumpsy – ThinkGeek Oct 21 '18 at 06:32
  • Possible duplicate of [Python: how to implement \_\_getattr\_\_()?](https://stackoverflow.com/questions/16237659/python-how-to-implement-getattr) – stovfl Oct 21 '18 at 07:17
  • @stovfl I dont know how you concluded it is a duplicate of __getattr__() question. I request you to read question and the upvoted answer thoroughly before concluding and marking something as duplicate. If you think it is a duplicate then please justify thay maybe in a single liner if it is not obvious. – ThinkGeek Oct 21 '18 at 11:50
  • Feel free to reject the dup as your solution. Read [Why are some questions marked as duplicate?](https://stackoverflow.com/help/duplicates) – stovfl Oct 21 '18 at 13:17

1 Answers1

12

I don't see what's clumpsy about this - most languages out there do not have built-in getter/setter validation so you would end up writing something similar to this in any language you choose - if you want to turn all your properties into getter/setter pairs with validation, you'll have to write them separately.

Now, Python is a dynamic language of a very flexible nature so there are several ways which you can use to reduce the verbosity (but not the complexity) of this process. For example, you can create your own validator decorator to be clamped on top of your setters, e.g.:

def validator(cmp, exc):
    def decorator(setter):  # a decorator for the setter
        def wrapper(self, value):  # the wrapper around your setter
            if cmp(value):  # if cmp function returns True, raise the passed exception
                raise exc
            setter(self, value)  # otherwise just call the setter to do its job
        return wrapper
    return decorator

And now you can define your getter/setter pairs with validation included as:

class User:

    def __init__(self, age):
        self.age = age

    @property
    def age(self):
        return self._age

    @age.setter
    @validator(lambda x: x < 0, ValueError("Age can't be negative"))
    def age(self, value):
        self._age = value

However, if you're only ever going to just do the validation and no other processing in your setters (and getters), you can just define your own validating property and save a lot on verbosity, something like:

class ValidatingProperty(object):

    def __init__(self, prop, cmp, exc):
        self.prop = prop
        self.cmp = cmp
        self.exc = exc

    def __get__(self, instance, owner=None):
        if instance is None:
            return self
        return getattr(instance, self.prop, None)

    def __set__(self, instance, value):
        if self.cmp(value):
            raise self.exc
        setattr(instance, self.prop, value)

    def __delete__(self, instance):
        delattr(instance, self.prop)

And now you can build your class getters/setters as simple as:

class User:

    age = ValidatingProperty("_age", lambda x: x < 0, ValueError("Age can't be negative"))

    def __init__(self, age):
        self.age = age

And if you ever need to access the raw property (assuming it was set), without wrappers around it, you can still access it with self._age (or whatever 'real' property that you've passed as the first argument to ValidatingProperty). You can even build your validators separately so you don't rely on lambdas (e.g. create an IntegerValidator class which lets you pass the ranges for validation and then reuse where needed).

The other option is to treat the users of your classes as adults and explain the valid values in the documentation and if they go outside of that - there be dragons. If the class is intended to be populated with data from end-users, the validation should be performed on the side that collects the end-user data (so that the end users can get a meaningful error with them in mind), not necessarily in the model itself.

zwer
  • 24,943
  • 3
  • 48
  • 66