Tuesday, June 19, 2007

Python: property attribute tricks

Here is my twist on the Python Cookbook recipe for class property definition: instead of using a non-standard doc="" variable for passing the documentation string to the property() function, I use a decorator that copies the doc-string from the decorated function and passes that to property() explicitly. First, my decorator:

def prop(func):
"""Function decorator for defining property attributes

The decorated function is expected to return a dictionary
containing one or more of the following pairs:
fget - function for getting attribute value
fset - function for setting attribute value
fdel - function for deleting attribute
This can be conveniently constructed by the locals() builtin
function; see:
http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/205183
"""
return property(doc=func.__doc__, **func())

An example of using this decorator to declare a managed attribute:

class MyExampleClass(object):
@prop
def foo():
"The foo property attribute's doc-string"
def fget(self):
return self._foo
def fset(self, value):
self._foo = value
return locals()

The only subtle difference between my method and the property attribute definition method described in the Python Cookbook is that my version allows you to specify document the attribute using python's standard doc-string syntax.

14 comments:

Doug Napoleone said...

Are you aware of python descriptors?

class MyDescriptor(object):
__"""the doc string"""
__def __init__(self, default=None):
____if default is not None:
______self.value = default
__def __set__(self, parent, value):
____self.value = value

__def __get__(self, instance, inst_class):
____## will raise AttributeError which will be treated properly and reported on teh parent not this instance.
____return self.value

__def __del__(self, instance):
____del self.value

class InstanceClass(object):
__attr1 = MyDescriptor(3)
__attr2 = MyDescriptor()
__attr4 = MyDescriptor("hello")


The benefit here is you do not need to write the same get/set calls over and over again as most properties are rather simple. The instance/instance_class information is also very handy.

Doug Napoleone said...

Correction:

the methods on the descriptor should be 'get', 'set', and 'del'. NOT '__get__', '__set__', and '__del__'. '__del__' is something else entirely...

Kelly Yancey said...

Thanks for the pointer! I have heard of descriptors but never actually seen them used in real life. Thanks to your tip, I finally sat down and did the research. I'm going to try refactoring some of my code that uses properties to use descriptors instead and see what kind of code size reduction I can achieve. Thanks again!

Kelly Yancey said...

OK, I'm pretty sure descriptors aren't particularly useful in the situation I described. For one, you need to pass the documentation string as a parameter to the descriptor's __init__ method and have it assign it to its own __doc__ attribute. This defeats the entire point of the recipe I posted: to be able to specify doc strings as usual. In addition, accessing instance attributes from the descriptor's __get__ and __set__ methods is a pain to do in a way that allows the descriptor to be re-usable across classes. I think I'm going to have to write a blog entry on the problem to cover it in detail...

David said...

I really like your recipe. Defining a decorator and handling the doc string is a nice refinement. You should add a comment to the Python Cookbook page with your code or a link to this article.

jjinux said...

Well done, sir!

Anonymous said...

With regard to previous comment I believe python descripters can only be used on class variables rather than instance variables.

Please prove me wrong this whole descriptors and property thing is giving me a headache.

Runsun said...

Please check out my recipe:

Easy Property Creation in Pythonhttp://code.activestate.com/recipes/576742/
Only 7 lines of code, easy to use, easy to understand, easy to customize, make the code much netter and will save you a lot of typing.

Brian Allbee said...

Very nice indeed! I've played around with a few different variations of this sort of decoration, but this (so far) is the one I like the most.

Only question I have boils down to: How does one unit-test these? In a case where I want/need a specific value-type, I haven't been able to figure out a way to completely test this sort of property except if it's being set directly...

Brian Allbee said...

To clarify my earlier question... Assume the following in a class:

__uri = None
@_property
def Uri():
...."""The uri of the namespace (e.g., the "uri" value in xmlns:name="uri" or )."""
....def fget( self ):
........return self.__uri
....@_strict( str )
....def fset( self, uri ):
........if ( type( uri ) <> types.StringType ):
............raise TypeError( "%s.Uri: Expected a string value for Uri." % ( self.__class__.__name__ ) )
........self.__uri = uri
....def fdel(self):
........del self.__uri
....return locals()

def __init__( name, uri ):
...."""Object cosntructor."""
....self.Name = name
....self.Uri = uri

Later, I want to unit-test the property itself, and the constructor, with the expectation that it will raise a TypeError if the Uri value is passed a non-string.

........testObject = XmlNamespace( "name", 1 )
........^^^ This should raise a TypeError, but doesn't
........testObject.Uri = 1
........^^^ This should raise a TypeError, but doesn't

I suspect that I'm missing something, but I have no idea what it is...

Kelly Yancey said...

@BDA: You might want to check your _strict() decorator. I suspect it may be eating your exception.

Brian Allbee said...

That got part of it. Cleaned up and all:

def _property(func):
..return property(doc=func.__doc__, **func())

import types

class Example( object ):
..__uri = None
..@_property
..def Uri():
...."""The uri of the namespace (e.g., the "uri" value in xmlns:name="uri" or )."""
....def fget( self ):
......return self.__uri
....def fset( self, uri ):
......if ( type( uri ) <> types.StringType ):
........raise TypeError( "%s.Uri: Expected a string value for Uri." % ( self.__class__.__name__ ) )
......self.__uri = uri
....def fdel(self):
......del self.__uri
....return locals()
..def __init__( self, name, uri ):
...."""Object cosntructor."""
....self.Name = name
....self.Uri = uri

class testExample( unittest.TestCase ):
..def testExampleUriConstructor( self ):
....# This one works fine, presumably because
....# the property-setter is wrapped inside another method...?
....self.assertRaises( TypeError, Example, "name", 2 )
..def testExampleSetUri( self ):
....testObject = Example( "name", "uri" )
....# This one doesn't, though - it throws an error:
....# "TypeError: 'int' object is not callable"
....# Is it even possible to test the property
....# in this fashion?
....self.assertRaises( testObject.Uri, 1 )

Brian Allbee said...
This comment has been removed by the author.
Brian Allbee said...

OK. I was a doofus, and missed the TypeError in the test-case that wasn't working. When that's back in place, it seems to be just fine, but when I change the test as follows to make sure that it *can* fail...

..def testExampleSetUri( self ):
....testObject = Example( "name", "uri" )
....self.assertRaises( TypeError, testObject.Uri, "ook" )

... it should raise a test-failure, since the value is valid, but the unit-test is expecting a TypeError, and isn't...?