Search code examples
pythonpython-3.xdecoratorpython-decorators

Specialized @property decorators in python


I have a few classes each of which has a number of attributes. What all of the attributes have in common is that they should be numeric properties. This seems to be an ideal place to use python's decorators, but I can't seem to wrap my mind around what the correct implementation would be. Here is a simple example:

class Junk(object):
    def __init__(self, var):
        self._var = var

    @property
    def var(self):
        """A numeric variable"""
        return self._var

    @var.setter
    def size(self, value):
        # need to make sure var is an integer
        if not isinstance(value, int):
            raise ValueError("var must be an integer, var = {}".format(value))
        self._var = value

    @var.deleter
    def size(self):
        raise RuntimeError("You can't delete var")

It seems to me that it should be possible to write a decorator that does everything so that the above can be transformed into:

def numeric_property(*args, **kwargs):
    ...

class Junk(object):
    def __init__(self, var):
        self._var = var

    @numeric_property
    def var(self):
        """A numeric variable"""
        return self._var

That way the new numeric_property decorator can be used in many classes.


Solution

  • A @property is just a special case of Python's descriptor protocol, so you can certainly build your own custom versions. For your case:

    class NumericProperty:
        """A property that must be numeric.
    
        Args:
          attr (str): The name of the backing attribute.
    
        """
    
        def __init__(self, attr):
            self.attr = attr
    
        def __get__(self, obj, type=None):
            return getattr(obj, self.attr)
    
        def __set__(self, obj, value):
            if not isinstance(value, int):
                raise ValueError("{} must be an integer, var = {!r}".format(self.attr, value))
            setattr(obj, self.attr, value)
    
        def __delete__(self, obj):
            raise RuntimeError("You can't delete {}".format(self.attr))
    
    class Junk:
    
        var = NumericProperty('_var')
    
        def __init__(self, var):
            self.var = var
    

    In use:

    >>> j = Junk('hi')
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "/Users/jonrsharpe/test.py", line 29, in __init__
        self.var = var
      File "/Users/jonrsharpe/test.py", line 17, in __set__
        raise ValueError("{} must be an integer, var = {!r}".format(self.attr, value))
    ValueError: _var must be an integer, var = 'hi'
    >>> j = Junk(1)
    >>> del j.var
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "/Users/jonrsharpe/test.py", line 21, in __delete__
        raise RuntimeError("You can't delete {}".format(self.attr))
    RuntimeError: You can't delete _var
    >>> j.var = 'hello'
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "/Users/jonrsharpe/test.py", line 17, in __set__
        raise ValueError("{} must be an integer, var = {!r}".format(self.attr, value))
    ValueError: _var must be an integer, var = 'hello'
    >>> j.var = 2
    >>> j.var
    2