Search code examples
pythonpython-3.xclasspropertiesgetter-setter

Python3 - Clarification about multiple verifications on the same property


I would like to do different verifications on the same property in Python and I am not sure exactly what is the right way to do this. For instance, in the code below, I would like to verify my obj's First and Last name for both (starts with capital letter and length is more than 2 symbols). For the last name property, I combined all checks in one setter. In this case, my code does not catch both exceptions simultaneously. If I split them just like in first name setter, I can't get it to run all the time. My question is what is the right way to achieve? thanks.

class Person:
    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name

    @property
    def last_name(self):
        return self.__last_name

    @last_name.setter
    def last_name(self, value):
        if len(value) > 2:
            self.__last_name = value
        else:
            raise Exception("Last name: more than 2 symbs needed")
        for letter in value:
            if letter == letter.upper():
                self.__last_name = value
                break
            else:
                raise Exception('Last name: has to start with upper case')

    @property
    def first_name(self):
        return self.__first_name

    @first_name.setter
    def first_name(self, value):
        for letter in value:
            if letter == letter.upper():
                self.__first_name = value
                break
            else:
                raise Exception('First name: has to start with upper case')

    @first_name.setter
    def first_name(self,value):
        if len(value) > 2:
            self.__first_name = value
        else:
            raise Exception("First name: more than 2 symbs needed")




Jack = Person('Ja', 'sparrow')
print(Jack.first_name)
print(Jack.last_name)

Solution

  • I've cleaned up your code a bit and made some changes for it to work properly. Also, I'd suggest you to raise ValueError() instead of Exception as this literally is an error in the value of first_name and last_name.

    It is possible to catch both errors in one single value by tweaking your code and printing the error message accordingly. However, you can't expect to catch errors in both first_name and last_name because as soon as the first exception occurs, the code stops executing, so the next Exception can't be caught and shown. This is simply because of the fact that python is an interpreted language and it stops executing at the first occurance of an error.

    Here is my version of your code.

    Assigning the value to self.first_name will actually call the method first_name() which has been decorated with @property.
    The same is true for self.last_name.

    This makes sure that the values are validated even inside __init__()

    The @property.setter methods set the values to variables named like _property; _first_name and _last_name.

    Having an underscore(_) before a variable shows the intent that the variable isn't supposed to be used publicly and is meant to be like an internal variable for the code.

    _first_name and _last_name hold the actual values of the object and the @property methods simply act as a kind of front end for the user to access(get and set) these values. As these are methods, validation can be easily done here.

    class Person:
        def __init__(self, first_name='John', last_name='Doe'):
            self.first_name = first_name
            self.last_name = last_name
    
        def __repr__(self):
            return f'{self.__class__.__name__}({self.first_name} {self.last_name})' 
    
        @property
        def last_name(self):
            return self._last_name
    
        @last_name.setter
        def last_name(self, value):
            msg = ''
            if len(value) <= 2:
                msg = 'Last name: more than 2 symbols needed'
    
            if not value[0].isupper():
                if not msg:
                    msg = 'Last name: has to start with upper case'
                else:
                    msg += ', has to start with upper case'
    
            if msg:
                raise ValueError(msg)
            else:
                self._last_name = value
    
        @property
        def first_name(self):
            return self._first_name
    
        @first_name.setter
        def first_name(self, value):
            msg = ''
            if len(value) <= 2:
                msg = 'First name: more than 2 symbols needed'
    
            if not value[0].isupper():
                if not msg:
                    msg = 'First name: has to start with upper case'
                else:
                    msg += ', has to start with upper case'
    
            if msg:
                raise ValueError(msg)
            else:
                self._first_name = value
    

    Here is an example of a valid Person object.

    >>> p = Person('Jack', 'Sparrow')
    >>> p
    Person(Jack Sparrow)
    

    These are the Exceptions caused while trying to create objects with invalid values.

    >>> p = Person('ja', 'Sparrow')
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "per.py", line 3, in __init__
        self.first_name = first_name
      File "per.py", line 47, in first_name
        raise ValueError(msg)
    ValueError: First name: more than 2 symbols needed, has to start with upper case
    
    >>> p = Person('Jack', 'sparrow')
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "per.py", line 4, in __init__
        self.last_name = last_name
      File "per.py", line 26, in last_name
        raise ValueError(msg)
    ValueError: Last name: has to start with upper case
    

    If both the first_name and last_name are invalid, only the Exception for first_name is raised because that is the value that gets assigned first in the code. Execution stops as soon as an Exception is raised, hence the error with the value of last_name is not caught anymore.

    >>> p = Person('jack', 'Sp')
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "per.py", line 3, in __init__
        self.first_name = first_name
      File "per.py", line 47, in first_name
        raise ValueError(msg)
    ValueError: First name: has to start with upper case