78

In python classes, the @property is a nice decorator that avoids using explicit setter and getter functions. However, it comes at a cost of an overhead 2-5 times that of a "classical" class function. In my case, this is quite OK in the case of setting a property, where the overhead is insignificant compared to the processing that needs to be done when setting.

However, I need no processing when getting the property. It is always just "return self.property". Is there an elegant way to use the setter but not using the getter, without needing to use a different internal variable?

Just to illustrate, the class below has the property "var" which refers to the internal variable "_var". It takes longer to call "var" than "_var" but it would be nice if developers and users alike could just use "var" without having to keep track of "_var" too.

class MyClass(object):
  def __init__(self):
    self._var = None

  # the property "var". First the getter, then the setter
  @property
  def var(self):
    return self._var
  @var.setter
  def var(self, newValue):
    self._var = newValue
    #... and a lot of other stuff here

  # Use "var" a lot! How to avoid the overhead of the getter and not to call self._var!
  def useAttribute(self):
    for i in xrange(100000):
      self.var == 'something'

For those interested, on my pc calling "var" takes 204 ns on average while calling "_var" takes 44 ns on average.

Jonas Lindeløv
  • 5,442
  • 6
  • 31
  • 54

4 Answers4

87

Don't use a property in this case. A property object is a data descriptor, which means that any access to instance.var will invoke that descriptor and Python will never look for an attribute on the instance itself.

You have two options: use the .__setattr__() hook or build a descriptor that only implements .__set__.

Using the .__setattr__() hook

class MyClass(object):
    var = 'foo'

    def __setattr__(self, name, value):
        if name == 'var':
            print "Setting var!"
            # do something with `value` here, like you would in a
            # setter.
            value = 'Set to ' + value
        super(MyClass, self).__setattr__(name, value)

Now normal attribute lookups are used when reading .var but when assigning to .var the __setattr__ method is invoked instead, letting you intercept value and adjust it as needed.

Demo:

>>> mc = MyClass()
>>> mc.var
'foo'
>>> mc.var = 'bar'
Setting var!
>>> mc.var
'Set to bar'

A setter descriptor

A setter descriptor would only intercept variable assignment:

class SetterProperty(object):
    def __init__(self, func, doc=None):
        self.func = func
        self.__doc__ = doc if doc is not None else func.__doc__
    def __set__(self, obj, value):
        return self.func(obj, value)

class Foo(object):
    @SetterProperty
    def var(self, value):
        print 'Setting var!'
        self.__dict__['var'] = value

Note how we need to assign to the instance .__dict__ attribute to prevent invoking the setter again.

Demo:

>>> f = Foo()
>>> f.var = 'spam'
Setting var!
>>> f.var = 'ham'
Setting var!
>>> f.var
'ham'
>>> f.var = 'biggles'
Setting var!
>>> f.var
'biggles'
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • Thanks a lot for a great response! I ended up using the __setattr__ hook because only it allowed operations on the value like "myInstance.attrib += 5". I can add that the __setattr__ is not overwritten if it is set in a class that extends another class. In terms of performance it adds around x3 overhead compared to @property setter, but of cause it is much faster to get an attribute value, which is way more important. – Jonas Lindeløv Jul 10 '13 at 19:34
  • Quick question. Would it work if you moved `self.__dict__['var'] = value` to the descriptor's `__set__` method as `obj.__dict__['var'] = value`? – Mad Physicist Dec 16 '16 at 16:27
  • @MadPhysicist: of course! `self` and `obj` reference the same object there. – Martijn Pieters Dec 16 '16 at 16:31
  • And more generally, could you do `obj.__dict__[self.func.__name__] = value`? Am I correct in expecting this to work whenever the annotated method is defined via `def`? – Mad Physicist Dec 16 '16 at 16:34
  • @MadPhysicist: you could, and yes, function objects created via `def` have a sane `__name__` attribute (via `lambda` the attribute is set too but not all that useful). – Martijn Pieters Dec 16 '16 at 16:35
  • Cool. Thanks for clarifying that. I was trying to make the setter do the actual setting, which it looks like it will. Best solution would probably be to pass the name to `SetterProperty.__init__` with a default of `None`, which gets converted immediately to `func.__name__`. – Mad Physicist Dec 16 '16 at 16:41
  • just FYI, I mostly ripped off your code for an answer here: http://stackoverflow.com/a/41189407/2988730 – Mad Physicist Dec 16 '16 at 17:39
  • 1
    Also related: https://docs.python.org/3.6/whatsnew/3.6.html#pep-487-descriptor-protocol-enhancements – Mad Physicist Dec 22 '16 at 21:47
44

property python docs: https://docs.python.org/2/howto/descriptor.html#properties

class MyClass(object):
    def __init__(self):
        self._var = None

    # only setter
    def var(self, newValue):
        self._var = newValue

    var = property(None, var)


c = MyClass()
c.var = 3
print ('ok')
print (c.var)

output:

ok
Traceback (most recent call last):
  File "Untitled.py", line 15, in <module>
    print c.var
AttributeError: unreadable attribute
WeizhongTu
  • 6,124
  • 4
  • 37
  • 51
  • 4
    This does not solve the problem since I still want the attribute to be "gettable". It's just that I didn't want any processing overhead on getting it. But interesting solution for other purposes! – Jonas Lindeløv Jan 29 '16 at 23:03
  • 2
    @JonasLindeløv Yes, this may be useful for someone, so I posted my answer here. – WeizhongTu Jan 30 '16 at 07:25
  • 2
    is there any way I can use this as decorator. I mean rather than using `var = property(None, var)` , I use decorator – Gaurang Shah Aug 18 '17 at 14:27
  • 7
    @WeizhongTu You can make the attribute readable by replacing `var = property(None, var)` with `var = property(lambda x: x._var, var)`. – Nuno André Sep 20 '17 at 22:55
6

The @WeizhongTu answer

class MyClass(object):
    def __init__(self):
        self._var = None

    # only setter
    def var(self, newValue):
        self._var = newValue

    var = property(None, var)


c = MyClass()
c.var = 3
print ('ok')
print (c.var)

Is fine, except from the fact that is making the variable ungettable...

A similar solution but preserving getter is with

var = property(lambda self: self._var, var)

instead of

var = property(None, var)
Lore
  • 1,286
  • 1
  • 22
  • 57
  • `var = property(lambda self: getattr(self, '_var', None), var)` saves you from having to define `self._var`. But usually, the best option will likely be to use your setter and not to define `self._var`. That way the object will raise an `AttributeError` if no value has been set, which is often to be expected. – Nuno André Jan 26 '21 at 19:08
2

The accepted answer's setter descriptor would be probably more convenient if it set the property by itself:

A setter descriptor (alt.)

class setter:
    def __init__(self, func, doc=None):
        self.func = func
        self.__doc__ = doc or func.__doc__

    def __set__(self, obj, value):
        obj.__dict__[self.func.__name__] = self.func(obj, value)

class Foo:
    @setter
    def var(self, value):
        print('Setting var!')

        # validations and/or operations on received value
        if not isinstance(value, str):
            raise ValueError('`var` must be a string')
        value = value.capitalize()

        # returns property value
        return value

Demo:

>>> f = Foo()
>>> f.var = 'spam'
Setting var!
>>> f.var = 'ham'
Setting var!
>>> f.var
'Ham'
>>> f.var = 'biggles'
Setting var!
>>> f.var
'Biggles'
>>> f.var = 3
ValueError: `var` must be a string
Nuno André
  • 4,739
  • 1
  • 33
  • 46