ActiveState Code

Recipe 410698: Property decorator for python 2.4


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
 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()

Discussion

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.

Comments

  1. 1. At 3:38 p.m. on 25 apr 2005, Ian Bicking said:

    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
    
  2. 2. At 4:17 p.m. on 25 apr 2005, George Sakkis (the author) said:

    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.

  3. 3. At 4:25 p.m. on 25 apr 2005, George Sakkis (the author) said:

    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.

  4. 4. At 9:11 p.m. on 27 may 2005, Benji York said:

    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())
    
  5. 5. At 9:14 a.m. on 8 jul 2005, Sean Ross said:

    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)
    
         ...
    
  6. 6. At 3:22 p.m. on 24 aug 2006, Walker Hale said:

    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()
    
  7. 7. At 11:49 a.m. on 6 may 2009, runsun pan said:

    Check out my recipe:

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

  8. 8. At 11:51 a.m. on 24 jul 2009, haridsv said:

    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.

  9. 9. At 4:21 p.m. on 24 jul 2009, haridsv said:

    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.

  10. 10. At 5:12 p.m. on 24 jul 2009, haridsv said:

    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.

Sign in to comment