'How to apply multiple similar properties to a class

I am learning about the @property decorator at the moment. Consider a sample class below, named Rectangle. It has 2 attributes, width and height, both of which should be strictly positive integers. I enforced this using the @property decorator.

class Rectangle:
    '''
    A simple class to model a rectangle.

    Parameters
    ----------
    width : int
        The width of the rectangle.
    height : int
        The height of the rectangle.

    Returns
    -------
    None.
    '''
    
    #---INITIALIZATION---------------------------------------------------------

    def __init__(self, width : int, height : int):
        self.width = width
        self.height = height
    
    #---PROPERTIES-------------------------------------------------------------
    
    @property
    def width(self):
        return self._width
    @width.setter
    def width(self, val : int):
        if isinstance(val, int) and val > 0:
            self._width = val
        else:
            raise ValueError('Width cannot be negative.')
    @width.deleter
    def width(self):
        del self._width

    @property
    def height(self):
        return self._height
    @height.setter
    def height(self, val : int):
        if isinstance(val, int) and val > 0:
            self._height = val
        else:
            raise ValueError('Height cannot be negative.')
    @height.deleter
    def height(self):
        del self._height    
    
    #---SETTERS----------------------------------------------------------------
    
    def set_width(self, val : int):
        '''
        Set the Rectangle width to any stricly positive integer value.
        '''
        self.width = val
            
    def set_height(self, val):
        '''
        Set the Rectangle height to any stricly positive integer value.
        '''
        self.height = val

This code works perfectly well. Instantiation (Rectangle(-5,3)), direct assignment (rect.width = -5) and the setter methods (rect.set_width(-5)) raise a ValueError when the dimension is not a strictly positive number. However, the decoration of these width and height attributes is very similar. Is there a clean way to generate a @property decorator template for different attributes with the same conditions (e.g. positive integers)?

Bonus question: I tried by creating a PositiveInteger class. This indeed blocks instantiation (Rectangle(-5,3)) and the setter methods (rect.set_width(-5)) but incorrect direct assignment (rect.width = -5) remains possible. But why exactly is this not working?

class PositiveInteger:
    '''
    A simple class to decorate an attribute with an @property decorator to
    enforce strictly positive values.

    Parameters
    ----------
    name : str
        The name of the attribute.
    value : int
        The set value of the attribute.

    Returns
    -------
    None.
    '''
    
    #---INITIALIZATION---------------------------------------------------------

    def __init__(self, name : str, value : int):
        self.name = name
        self.value = value
        
    #---PROPERTIES-------------------------------------------------------------
    
    @property
    def value(self):
        return self._value
    @value.setter
    def value(self, val : int):
        if isinstance(val, int) and val > 0:
            self._value = val
        else:
            raise ValueError(f'{self.name.capitalize()} cannot be negative.')
    @value.deleter
    def value(self):
        del self._value

#==============================================================================

class Rectangle:
    '''
    A simple class to model a rectangle.

    Parameters
    ----------
    width : int
        The width of the rectangle.
    height : int
        The height of the rectangle.

    Returns
    -------
    None.
    '''
    
    #---INITIALIZATION---------------------------------------------------------

    def __init__(self, width : int, height : int):
        self.width = PositiveInteger('width', width).value
        self.height = PositiveInteger('height', height).value
                
    #---SETTERS----------------------------------------------------------------
    
    def set_width(self, val : int):
        '''
        Set the Rectangle width to any stricly positive integer value.
        '''
        self.width = PositiveInteger('width', val).value
            
    def set_height(self, val):
        '''
        Set the Rectangle height to any stricly positive integer value.
        '''
        self.height = PositiveInteger('height', val).value

Thanks in advance! P.S. I'm loving the Stack Overflow redesign.



Solution 1:[1]

My own answer After some extra days of Googling, I discovered descriptor classes in the Python documentation. Once instantiated in an external class, a descriptor class manages the getting and setting of attributes of the external class. This seems to work for instantiation, direct assignment and the setter methods. The only way unwanted changes can happen is if the private names of the attributes are accessed, e.g., rect._height = -5.

#==============================================================================

class PositiveIntegerDescriptor: 
    '''
    A descriptor class to enforce an attribute to be a strictly positive integer.

    Parameters
    ----------
    name : str
        The name of the attribute.

    Returns
    -------
    None.
    '''
    
    #--INITIALIZATION----------------------------------------------------------
    
    def __init__(self, name : str):
        self.public_name = name
        self.private_name = '_' + name
            
    #--GETTERS AND SETTERS-----------------------------------------------------

    def __get__(self, obj, objtype=None):
        return getattr(obj, self.private_name)
    
    def __set__(self, obj, new_value):
        if isinstance(new_value, int) and new_value > 0:
            setattr(obj, self.private_name, new_value)
        else:
            raise ValueError(f'{self.public_name.capitalize()} must be a strictly positive integer.')

#==============================================================================

class Rectangle:
    '''
    A simple class to model a rectangle.

    Parameters
    ----------
    width : int
        The width of the rectangle.
    height : int
        The height of the rectangle.

    Returns
    -------
    None.
    '''
    
    #---DESCRIPTORS------------------------------------------------------------
    
    width = PositiveIntegerDescriptor('width')
    height = PositiveIntegerDescriptor('height')
    
    #---INITIALIZATION---------------------------------------------------------

    def __init__(self, width : int, height : int):
        self.width  = width
        self.height = height
   
    #---SETTERS----------------------------------------------------------------
    
    def set_width(self, value : int):
        '''
        Set the Rectangle width to any stricly positive integer value.
        '''
        self.width = value
            
    def set_height(self, value):
        '''
        Set the Rectangle height to any stricly positive integer value.
        '''
        self.height = value
    
#==============================================================================

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Ben Zeen