Enhanced property decorator

Daniel millerdev at gmail.com
Mon Aug 25 21:45:36 EDT 2008


I've often been frustrated by the inability of the built-in property
descriptor to handle anything other than a read-only property when
used as a decorator. Furthermore, read/write/delete properties take
their doc-string and property definition at a non-intuitive and
awkward place (after the getter/setter/delter functions). The
following are three possible solutions to this problem (inspired by
message http://groups.google.com/group/comp.lang.python/msg/9a56da7ca8ceb7c7).
I don't like the solution in that thread because it uses apply() which
will go away in Python 3.

Solution 1: new built-in function/descriptor

def prop(func):
    funcs = dict(enumerate(func()))
    return property(funcs[0], funcs[1], funcs.get(2), func.__doc__)

class Test(object):
    @prop
    def test():
        """test doc string"""
        def fget(self):
            return self._test
        def fset(self, value):
            self._test = value
        def fdel(self):
            del self._test
        return fget, fset, fdel

Of course the name (prop) could be changed... I couldn't think of
anything more concise.

Pros:
(1) encapsulation of property logic inside function namespace,
preventing clutter in class namespace.
(2) doc string appears in a more natural place, before getter/setter/
delter logic, as in classes and functions.

Cons:
(1) additional built-in name.
(2) duplication/boilerplate in return line (DRY violation).


Solution 2: Enhance the built-in property descriptor, so if it
receives a generator function taking no arguments as its first and
only argument it treats the generated result similar to the above prop
function. Example:

import types

def property_(fget, fset=None, fdel=None, doc=None):
    if fget is not None and fset is None and fdel is None and doc is
None \
        and fget.func_code.co_argcount == 0:
        # is there a way to detect a generator function without
calling it?
        gen = fget()
        if isinstance(gen, types.GeneratorType):
            result = list(gen)
            assert len(result) == 1, "<oops! generated wrong number of
results>"
            funcs = dict(enumerate(result[0]))
            doc = fget.__doc__
            fget = funcs[0]
            fset = funcs[1]
            fdel = funcs.get(2)
    # normal property logic follows
    return property(fget, fset, fdel, doc)

class Test(object):
    @property_
    def test():
        """test doc string"""
        def fget(self):
            return self._test
        def fset(self, value):
            self._test = value
        def fdel(self):
            del self._test
        yield fget, fset, fdel

Pros:
(1) overloaded use of existing property descriptor prevents built-in
clutter.
(2) encapsulation of property logic inside function namespace,
preventing clutter in class namespace.
(3) doc string appears in a more natural place, before getter/setter/
delter logic, as in classes and functions.

Cons:
(1) possible unexpected behavior when zero-argument function (fget) is
called.
(2) duplication/boilerplate in yield line (DRY violation).

It would take two "errors" to get the "unexpected" behavior in this
second solution. First, property getters normally (always?) take a
'self' argument. Second, the function must be a generator yielding a
single result (not a common type of property getter). It would be best
to have some way of knowing if the function is in fact a generator
before calling it since that would result in the least amount of
surprise.


Solution 3: Possibly the most controversial, but arguably the most
elegant solution:

class Test(object):
    @property
    def test():
        """test doc string"""
        yield(self):
            return self._test
        yield(self, value):
            self._test = value
        yield(self):
            del self._test

This option would require new syntax for the yield keyword, which
would yield an unnamed function object (similar to a lambda object).
The important part is that the no-argument generator function passed
to property would yield up to three callables, which would be used in
order as the getter/setter/delter for the property. The doc string for
the property would be the doc string from the generator function. An
implementation of the additional property logic needed for this
solution is postponed until there is sufficient interest.

Pros:
(1) overloaded use of existing property descriptor prevents built-in
clutter.
(2) encapsulation of property logic inside function namespace,
preventing clutter in class namespace.
(3) doc string appears in a more natural place, before getter/setter/
delter logic, as in classes and functions.

Cons:
difficult to implement?

Of course, more error checking would be necessary if any these
enhancements were added to the standard library.

Thoughts?

~ Daniel



More information about the Python-list mailing list