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.

Ian Bicking 16 years, 9 months ago

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

'''The angle in radians'''
def fget(self):
def fset(self,angle):
if isinstance(angle,Angle): angle = angle.rad
``````
George Sakkis (author) 16 years, 9 months ago

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
``````

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.

George Sakkis (author) 16 years, 9 months ago

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
``````

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 16 years, 8 months ago

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 16 years, 6 months ago

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

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

...
``````
Walker Hale 15 years, 5 months ago

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 12 years, 8 months ago

Check out my recipe:

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

haridsv 12 years, 6 months ago

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 12 years, 6 months ago

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 12 years, 6 months ago

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 12 years ago

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 12 years ago

Please ignore the 20-Jan-10 comment above.

Colin W.

 Created by George Sakkis on Mon, 25 Apr 2005 (PSF)