67

So, I'm trying to figure out the best (most elegant with the least amount of code) way to allow overriding specific functions of a property (e.g., just the getter, just the setter, etc.) in python. I'm a fan of the following way of doing properties, due to the fact that all of their methods are encapsulated in the same indented block of code (it's easier to see where the functions dealing with one property stop and the functions dealing with the next begin):

@apply
def foo():
    """A foobar"""
    def fget(self):
        return self._foo
    def fset(self, val):
        self._foo = val
    return property(**locals())

However, if I want to inherit from a class that defines properties in this manner, and then, say, override the foo setter function, it seems tricky. I've done some searching and most of the answers I've found have been to define separate functions in the base class (e.g. getFoo and setFoo), explicitly create a property definition from them (e.g. foo = property(lambda x: x.getFoo(), lambda x, y: x.setFoo(y), lambda x: x.delFoo())), and then override getFoo, setFoo, and delFoo as needed.

I dislike this solution because it means I have to define lambas for every single property, and then write out each function call (when before I could have just done property(**locals())). I also don't get the encapsulation that I had originally.

Ideally, what I would like to be able to do would be something like this:

class A(object):
    def __init__(self):
        self.foo = 8
    @apply
    def foo():
        """A foobar"""
        def fget(self):
            return self._foo
        def fset(self, val):
            self._foo = val
        return property(**locals())

class ATimesTwo(A):
    @some_decorator
    def foo():
        def fset(self, val):
            self._foo = val * 2
        return something

And then the output would look something like:

>>> a = A()
>>> a.foo
8
>>> b = ATimesTwo()
>>> b.foo
16

Basically, ATimesTwo inherits the getter function from A but overrides the setter function. Does anybody know of a way to do this (in a manner that looks similar to the example above)? What function would the some_decorator look like, and what should the foo function return?

Jessica Hamrick
  • 773
  • 1
  • 5
  • 6
  • i can't see any way to do what you want with a decorator alone because it doesn't have access to the object, only to the function. i guess you you have a decorator that returns a function that does further work when the method is first called, but then you have overwritten the initial foo. also - as i guess you know - you're not really using the language in the style intended... but you might find a way to do this with a metaclass. – andrew cooke Aug 11 '11 at 01:35
  • This similar question may be helpful: http://stackoverflow.com/questions/237432/python-properties-and-inheritance – stderr Aug 11 '11 at 03:34

3 Answers3

82

The Python docs on the property decorator suggest the following idiom:

class C(object):
    def __init__(self):
        self._x = None
    @property
    def x(self):
        return self._x
    @x.setter
    def x(self, value):
        self._x = value
    @x.deleter
    def x(self):
        del self._x

And then subclasses can override a single setter/getter like this:

class C2(C):
    @C.x.getter
    def x(self):
        return self._x * -1

This is a little warty because overriding multiple methods seems to require you to do something like:

class C3(C):
    @C.x.getter
    def x(self):
        return self._x * -1
    # C3 now has an x property with a modified getter
    # so modify its setter rather than C.x's setter.
    @x.setter 
    def x(self, value):
        self._x = value * 2

Of course at the point that you're overriding getter, setter, and deleter you can probably just redefine the property for C3.

stderr
  • 8,567
  • 1
  • 34
  • 50
  • 4
    Thanks a bunch for the `C.x.getter` associated with `x.setter`, could not find this trick anywhere else and this is definitely something that should be changed because it is hell of confusing ! It requires insight into property objects which I did not possess. +1 and this should be the accepted answer because it is the most complete one. – Valentin B. Dec 14 '18 at 09:48
61

I'm sure you've heard this before, but apply has been deprecated for eight years, since Python 2.3. Don't use it. Your use of locals() is also contrary to the Zen of Python -- explicit is better than implicit. If you really like the increased indentation, there is no need to create a throwaway object, just do

if True:
    @property
    def foo(self):
        return self._foo
    @foo.setter
    def foo(self, val):
        self._foo = val

Which doesn't abuse locals, use apply, require creation of an extra object, or need a line afterwards with foo = foo() making it harder to see the end of the block. It works just as well for your old-fashioned way of using property -- just do foo = property(fget, fset) as normal.

If you want to override a property in an arbitrary subclass, you can use a recipe like this.

If the subclass knows where the property was defined, just do:

class ATimesTwo(A):
    @A.foo.setter
    def foo(self, val):
        self._foo = val * 2
agf
  • 171,228
  • 44
  • 289
  • 238
  • 8
  • 2
    @Jean-FrançoisCorbett: It's just a way to be able to indent the block of code that follow it that little or no other side-effects. – martineau May 21 '19 at 00:15
  • 1
    @Jean-FrançoisCorbett I totally agree. Indentation being part of the syntax in python, I don't see any good reason to want to play with it in a non-standard way. Actually, reducing the necessary indentation is the usual problem, not having too much of it... stuff like trying to implement a switch statement have been discussed in PEPs and one of the reasons why python still doesn't have a switch statement is because one option is to write switch(variable) and then indent case(variable value) and then another indent within the case, and that was considered undesirable. So... yeah. I don't see it – Patrick Da Silva Feb 19 '20 at 10:42
  • 1
    Note if you want deleter in addition to setter, then the decorator for the deleter must be `@foo.deleter`, not `@A.foo.deleter`, because the latter would define a property without the setter defined just above the deleter. – fumito May 16 '20 at 16:27
9

The answer of stderr satisfies most use cases.

I'd like to add a solution for the case where you want to extend a getter, setter and/or deleter. Two ways to do this are:

1. Subclass property

First way to do this is by subclassing the builtin property and adding decorators that are versions of getter, setter and/or deleter that extend the current get, set and delete callbacks

Example for a property that supports appending methods to the set-functions:

class ExtendableProperty(property):
    def append_setter(self, fset):
        # Create a wrapper around the new fset that also calls the current fset
        _old_fset = self.fset

        def _appended_setter(obj, value):
            _old_fset(obj, value)
            fset(obj, value)
        # Use that wrapper as setter instead of only the new fset
        return self.setter(_appended_setter)

Usage is the same as for normal properties, only now it is possible to add methods to the property setters:

class A(object):
    @ExtendableProperty
    def prop(self):
        return self._prop

    @prop.setter
    def prop(self, v):
        self._prop = v


class B(A):
    @A.prop.append_setter
    def prop(self, v):
        print('Set', v)
>>> a = A()
>>> a.prop = 1
>>> a.prop
1

>>> b = B()
>>> b.prop = 1
Set 1
>>> b.prop
1

2. Overwrite getter, setter and/or deleter

Use a normal property, overwrite the getter, setter or deleter and then add calls to the fget, fset or fdel in the property of the parent class.

Example for the type of property as in example 1:

class A(object):
    @property
    def prop(self):
        return self._prop

    @prop.setter
    def prop(self, v):
        self._prop = v


class B(A):
    @A.prop.setter
    def prop(self, v):
        A.prop.fset(self, v)  # This is the call to the original set method
        print('Set {}'.format(v))

I think the first option looks nicer because the call to the super property's fset is not necessary

jusx
  • 1,099
  • 9
  • 14