Welcome, guest | Sign In | My Account | Store | Cart

This recipe refines an older recipe on creating class properties ( http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/205183). The refinement consists of: - Using a decorator (introduced in python 2.4) to "declare" a function in class scope as property. - Using a trace function to capture the locals() of the decorated function, instead of requiring the latter to return locals(), as in the older recipe.

Python, 56 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
def test():
    from math import radians, degrees, pi

    class Angle(object):
        def __init__(self,rad):
            self._rad = rad

        @Property
        def rad():
            '''The angle in radians'''
            def fget(self):
                return self._rad
            def fset(self,angle):
                if isinstance(angle,Angle): angle = angle.rad
                self._rad = float(angle)


        @Property
        def deg():
            '''The angle in degrees'''
            def fget(self):
                return degrees(self._rad)
            def fset(self,angle):
                if isinstance(angle,Angle): angle = angle.deg
                self._rad = radians(angle)


    def almostEquals(x,y):
        return abs(x-y) < 1e-9

    a = Angle(pi/3)
    assert a.rad == pi/3 and almostEquals(a.deg, 60)
    a.rad = pi/4
    assert a.rad == pi/4 and almostEquals(a.deg, 45)
    a.deg = 30
    assert a.rad == pi/6 and almostEquals(a.deg, 30)
    print Angle.rad.__doc__
    print Angle.deg.__doc__


def Property(function):
    keys = 'fget', 'fset', 'fdel'
    func_locals = {'doc':function.__doc__}
    def probeFunc(frame, event, arg):
        if event == 'return':
            locals = frame.f_locals
            func_locals.update(dict((k,locals.get(k)) for k in keys))
            sys.settrace(None)
        return probeFunc
    sys.settrace(probeFunc)
    function()
    return property(**func_locals)


if __name__ == '__main__':
    test()

As in the original recipe, defining a property involves the definition of nested functions for one or more of fget,fset,fdel. The decorator probes the decorated function, captures its locals() just before it returns and looks for the names "fget", "fset" and "fdel". The found values, along with the function's docstring are passed to property() and the resulting property is bounded to the decorated function's name.

12 comments

Ian Bicking 19 years ago  # | flag

class/metaclass. I think a metaclass is more useful and easier to read (in use, not necessarily in implementation) for this kind of thing. I've posted an example of this in my repository: http://svn.colorstudy.com/home/ianb/recipes/class_property.py <p>

I'll copy the actual code here:

real_property = property

class property_meta(type):

    def __new__(meta, class_name, bases, new_attrs):
        if bases == (object,):
            # The property class itself
            return type.__new__(meta, class_name, bases, new_attrs)
        fget = new_attrs.get('fget')
        fset = new_attrs.get('fset')
        fdel = new_attrs.get('fdel')
        fdoc = new_attrs.get('__doc__')
        return real_property(fget, fset, fdel, fdoc)

class property(object):

    __metaclass__ = property_meta

    def __new__(cls, fget=None, fset=None, fdel=None, fdoc=None):
        if fdoc is None and fget is not None:
            fdoc = fget.__doc__
        return real_property(fget, fset, fdel, fdoc)

A brief discussion: this is backward compatible, because property.__new__ produces a normal property instance when you call it (__new__ keeps property() from returning an instance of itself, instead returning an instances of the real property class). The metaclass causes subclasses of this custom property to return property instances again, instead of real subclasses. (There's a special case that keeps the property class itself from returning an property instances -- bases == (object,)). Though the use of "class" is unfortunate, I think this is otherwise an ideal syntax for creating properties. I wrote this code, but I know I've seen similar implementations elsewhere, so I can't claim it's my own novel idea. Ultimately you use it like:

class Angle(object):
    def __init__(self,rad):
        self._rad = rad

    class rad(property):
        '''The angle in radians'''
        def fget(self):
            return self._rad
        def fset(self,angle):
            if isinstance(angle,Angle): angle = angle.rad
            self._rad = angle

The two approaches (decorator vs metaclass) are pretty similar in usage, although completely different in implementation. The main difference in usage is the property "signature":

@Property
def rad():

with decorator versus

class rad(property):

with metaclass. None is IMO as good as a special property syntax or code blocks would allow, e.g. something like

rad = property:
    def fget(self): ...
    def fset(self): ...

I find decorators are closer though by being more explicit; the class declaration would be misleading to anyone not familiar with the mutated property().

An advantage of the metaclass solution is the backwards compatibility with the builtin property(); with the decorator, a new name ("Property") has to be defined.

The two approaches (decorator vs metaclass) are pretty similar in usage, although completely different in implementation. The main difference in usage is the property "signature":

@Property
def rad():

with decorator versus

class rad(property):

with metaclass. None is IMO as good as a special property syntax or code blocks would allow, e.g. something like

rad = property:
    def fget(self): ...
    def fset(self): ...

I find decorators are closer though by being more explicit; the class declaration would be misleading to anyone not familiar with the mutated property().

An advantage of the metaclass solution is the backwards compatibility with the builtin property(); with the decorator, a new name ("Property") has to be defined.

Benji York 18 years, 11 months ago  # | flag

Alternate without a new decorator. Here's a way that doesn't require any new decorators:

class Example(object):

    @apply
    def myattr():
        doc = """This is the doc string."""

        def fget(self):
            return self._value

        def fset(self, value):
            self._value = value

        def fdel(self):
            del self._value

        return property(**locals())
Sean Ross 18 years, 9 months ago  # | flag

backward compatible change to property. Your improvement on my recipe is great. Thanks.

I just wanted to suggest that it's possible to change the original property to take on this decorating behaviour in a backwards compatible way:

import sys

# changed name to use published cookbook idiom and to aid clarity
def nested_property(function):
    keys = 'fget', 'fset', 'fdel'
    func_locals = {'doc':function.__doc__}
    def probe_function(frame, event, arg):
        if event == 'return':
            locals = frame.f_locals
            func_locals.update(dict((k,locals.get(k)) for k in keys))
            sys.settrace(None)
        return probe_function
    sys.settrace(probe_function)
    function()
    return property(**func_locals)

old_property = property
def property(fget=None, fset=None, fdel=None, doc=None, nested=False):
  if nested:
    return nested_property
  else:
    return old_property(fget, fset, fdel, doc)

...


class Angle(object):
    def __init__(self,rad):
        self._rad = rad

    @property(nested=True)
    def rad():
        '''The angle in radians'''
        def fget(self):
            return self._rad
        def fset(self,angle):
            if isinstance(angle,Angle): angle = angle.rad
            self._rad = float(angle)

     ...
Walker Hale 17 years, 8 months ago  # | flag

Apply is deprecated, and sys.settrace should not be abused this way. The apply function is deprecated. This is an abuse of sys.settrace, which should only be used for debuggers, profilers, and code coverage tools. (Also sys.settrace is implementation dependent.)

Abandoning both, you can accomplish something easier than the apply technique:

def newProp( fcn ):
    return property( **fcn() )

class Example(object):

    @newProp
    def myattr():
        doc = """This is the doc string."""

        def fget(self):
            return self._value

        def fset(self, value):
            self._value = value

        def fdel(self):
            del self._value

        return locals()
runsun pan 14 years, 11 months ago  # | flag

Check out my recipe:

Easy Property Creation in Python
http://code.activestate.com/recipes/576742/

haridsv 14 years, 9 months ago  # | flag

One drawback I see with these approaches is that it removes the ability for derived classes to override and customize a particular method (such as a setter). In the traditional method, you still have a setter method in the base class that can be overridden to do something extra in the derived class in addition to what the base class does. On the other hand this does a pretty good job of encapsulation, so this could be a good thing depending on the situation.

haridsv 14 years, 9 months ago  # | flag

Never mind what I said above, it seems like the property() hardcodes the method object from the base class, so even if derives classes override one of the accessors, the property will continue to use the base class methods.

haridsv 14 years, 9 months ago  # | flag

Apologies for spamming, but needed to clarify the above as I found a workaround. If you use a lambda function instead of calling accessor directly, it will work just fine. E.g., "property(lambda self: self.getx(), lambda self, v: self.setx(v))" instead of "property(getx, setx)". Derived classes can then override getx/setx and it will work just fine.

Colin J. Williams 14 years, 3 months ago  # | flag

I've tried this with Python 2.6.4, with the result below. I get a similar result with Python 3.1.

Where have I gone astray?

Colin W.

* Python 2.6.4 (r264:75708, Oct 26 2009, 08:23:19) [MSC v.1500 32 bit (Intel)] on win32. *

>>> 
[Dbg]>>> 
Traceback (most recent call last):
  File "<string>", line 129, in run
  File "C:\Python26\Lib\bdb.py", line 368, in run
    exec cmd in globals, locals
  File "C:\Documents and Settings\cjw\My Documents\My Downloads\StockData\recipe-410698-1.py", line 56, in <module>
    test()
  File "C:\Documents and Settings\cjw\My Documents\My Downloads\StockData\recipe-410698-1.py", line 4, in test
    class Angle(object):
  File "C:\Documents and Settings\cjw\My Documents\My Downloads\StockData\recipe-410698-1.py", line 8, in Angle
    @Property
  File "C:\Documents and Settings\cjw\My Documents\My Downloads\StockData\recipe-410698-1.py", line 52, in Property
    return property(**func_locals)
TypeError: property() got an unexpected keyword argument 'doc'



>>> 
[PM]>>>
Colin J. Williams 14 years, 3 months ago  # | flag

Please ignore the 20-Jan-10 comment above.

Colin W.

Created by George Sakkis on Mon, 25 Apr 2005 (PSF)
Python recipes (4591)
George Sakkis's recipes (26)

Required Modules

Other Information and Tasks