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.
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.
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:
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:
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":
with decorator versus
with metaclass. None is IMO as good as a special property syntax or code blocks would allow, e.g. something like
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":
with decorator versus
with metaclass. None is IMO as good as a special property syntax or code blocks would allow, e.g. something like
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.
Alternate without a new decorator. Here's a way that doesn't require any new decorators:
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:
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:
Check out my recipe:
Easy Property Creation in Python
http://code.activestate.com/recipes/576742/
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.
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.
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.
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. *
Please ignore the 20-Jan-10 comment above.
Colin W.