Python Decorators – Should a Property Decorator Be Used Without a Setter or Deleter?

decoratorpython

I have a class that contains an object (set in this case). The set is modified by the standard function operators __add__ and __sub__, but I also want to explicitly expose a method of getting this set.

Should the @property decorator be used instead of a simple get() method, even if there is no intent to allow setting or deleting?

For example:

class A(object):

    def __init__(self):
        self.attribute = set() 

    def __add__(self, other):
        self.attribute.add(other)
        return self

    def __sub__(self, other):
        self.attribute.remove(other)
        return self

    @property
    def stuff(self):
        return self.attribute

It feels semantically correct to use a property and the right error messages are raised if an attempt to set or delete the property occurs AttributeError: can't set attribute. But, it also feels like a bit of a con to potential users who, knowing that a property exists, may expect the rest of the contract (setting and deleting) to be fulfilled.

Another option might be to define the setter/deleter in terms of __add__ and __sub__ but this seems like a less obvious breach of the same contract.

@stuff.setter
def stuff(self, other):
    self.__add__(other)

Best Answer

Read-only properties are perfectly fine. They are one of the very few Python mechanisms to offer some kind of encapsulation.

It is not terribly important whether you use a property or a getter method, as the difference is purely syntactic. However, they suggest slightly different semantics: a property should be field-like and provide you direct access to a value (which may be computed on the fly). A method may be more involved, and has the opportunity to receive additional parameters.

It is best not to put excessively clever behaviour into your properties, such as appending upon assigning. That just invites difficult to debug problems, as the contract of this property is not obvious. In fact, any kind of operator overloading should be looked at carefully. E.g. in foo(a + x) where a = A(), you would be modifying the a object! That is quite unexpected. You might want to overload __iadd__ instead, so that you must use a += x; foo(a). If you want a fluent interface so that you can modify your object in an expression, then maybe prefer a method name with a telling name that suggests in-place modification, e.g. foo(a.add(x)).