descriptor & docstring

C

cyril giraudon

Hello,

I try to use python descriptors to define attributes with default
value (the code is reported below).
But apparently, it breaks the docstring mechanism.

help(Basis) shows the right help but help(Rectangle) shows only two
lines :
"
Help on class Rectangle in module basis2:

Rectangle = <class 'basis2.Rectangle'>
"
If the Rectangle.length attribute is removed, the help is OK.

Secondly, the __doc__ attribute of a PhysicalValue instance doesn't
seem to be read.

I don't understand.

Any idea ?

Thanks a lot

Cyril.



# A descriptor class with default value handling
class PhysicalValue(object):
"""
A physical value descriptor
"""
def __init__(self, default_value):
self.default_value = default_value
self.__doc__ = "Hello from Physical Value"

def __get__(self, obj, type=None):
if obj.__dict__.has_key(self.key):
return getattr(obj, self.key)
else:
return self.default_value

def __set__(self, obj, value):
if value is DefaultValue:
setattr(obj, self.key, self.default_value)
else:
setattr(obj, self.key, value)

# A meta class which adds instance attributes
# If hasattr(cls, "attr") then add "_attr" attribute.
class MyMetaClass(type):
def __init__(cls, name, bases, dct):
super(MyMetaClass, cls).__init__(name, bases, dct)
print "Add property to ", name
def init(self):
pvl = [item for item in cls.__dict__.items()
if isinstance(item[1], PhysicalValue)]
for pv in pvl:
print "Add _%s property to %s" % (pv[0], name)
cls.__dict__[pv[0]].key = "_" + pv[0]
setattr(self, "_" + pv[0], getattr(self, pv[0]))
cls.__init__ = init

# A basis class
class Basis(object):
"""
Tempest basis class
"""
__metaclass__ = MyMetaClass

# A concrete class
class Rectangle(Basis):
"""
A beautiful Rectangle
"""
length = PhysicalValue(12.)
 
G

Gabriel Genellina

En Mon, 28 Apr 2008 14:35:40 -0300, cyril giraudon
Hello,

I try to use python descriptors to define attributes with default
value (the code is reported below).
But apparently, it breaks the docstring mechanism.

help(Basis) shows the right help but help(Rectangle) shows only two
lines :
"
Help on class Rectangle in module basis2:

Rectangle = <class 'basis2.Rectangle'>
"
If the Rectangle.length attribute is removed, the help is OK.

Secondly, the __doc__ attribute of a PhysicalValue instance doesn't
seem to be read.

I don't understand.

Any idea ?

This looks like a soup of descriptors, metaclasses and properties...
I'll write a two step example. I assume that you want to define an
attribute with a default value: when not explicitely set, it returns the
default value. This can be implemented with an existing descriptor:
property. The only special thing is to handle the default value.

Step 1: Our first attempt will let us write something like this:

class X(object:
length = property_default("length", 12., "This is the length property")

We have to write property_default so it returns a property object with the
right fget and fset methods. Let's use the same convention as your code,
property "foo" will store its value at attribute "_foo".

def property_default(prop_name, default_value=None, doc=None):

attr_name = '_'+prop_name

def fget(self, attr_name=attr_name,
default_value=default_value):
return getattr(self, attr_name, default_value)

def fset(self, value,
attr_name=attr_name,
default_value=default_value):
if value == default_value:
delattr(self, attr_name)
else:
setattr(self, attr_name, value)

return property(fget=fget, fset=fset, doc=doc)

When setting the same value as the default, the instance attribute is
removed (so the default will be used when retrieving the value later). I
think this is what you intended to do.
That's all. The classes look like this:

# just to use a more meaningful name, if you wish
PhysicalValue = property_default

# A basis class
class Basis(object):
"""
Tempest basis class
"""

# A concrete class
class Rectangle(Basis):
"""
A beautiful Rectangle
"""
length = PhysicalValue("length", 12., "This is the length property")

py> r = Rectangle()
py> print r.length
12.0
py> r.length = 13.5
py> print r.length
13.5
py> dir(r)
['__class__', ... '_length', 'length']
py> r.length = 12
py> dir(r)
['__class__', ... 'length']

Help works too:

py> help(Rectangle)
Help on class Rectangle in module __main__:

class Rectangle(Basis)
| A beautiful Rectangle
|
| Method resolution order:
| Rectangle
| Basis
| __builtin__.object
| [...]

py> help(Rectangle.length)
Help on property:

This is the length property



Step 2: The problem with the property_default declaration above is that it
repeats the name "length". If we want to comply with the DRY principle, we
can use a metaclass (note that the previous solution doesn't require a
custom metaclass). In the class declaration, we only have to store the
parameters needed to define the property; later, when the class is created
(the metaclass __new__ method), we replace those parameters with an actual
property object. The fget/gset implementation is the same as above.

class property_default(object):
"""Store info for defining a property with a default value.
Replaced with a real property instance at class creation time.
"""
def __init__(self, default_value, doc=None):
self.default_value = default_value
self.doc = doc

# just to use a more meaningful name, if you wish
class PhysicalValue(property_default): pass

class PropDefaultMetaClass(type):
def __new__(cls, name, bases, dct):
# replace all property_default declarations
# with an actual property object
# (we can't modify dct at the same time
# we iterate over it, so collect the new items
# into another dictionary)
newprops = {}
for prop_name, prop in dct.iteritems():
if isinstance(prop, property_default):
attr_name = '_'+prop_name
def fget(self, attr_name=attr_name,
default_value=prop.default_value):
return getattr(self, attr_name, default_value)
def fset(self, value,
attr_name=attr_name,
default_value=prop.default_value):
if value == default_value:
delattr(self, attr_name)
else:
setattr(self, attr_name, value)
newprops[prop_name] = property(
fget=fget, fset=fset,
doc=prop.doc)
dct.update(newprops)
return super(MyMetaClass, cls).__new__(cls, name, bases, dct)

# A basis class
class Basis(object):
"""
Tempest basis class
"""
__metaclass__ = PropDefaultMetaClass

# A concrete class
class Rectangle(Basis):
"""
A beautiful Rectangle
"""
length = PhysicalValue(12., "This is the length property")

The usage and behavior is the same as in step 1, only that we can omit the
"length" parameter to PhysicalValue.
 
G

George Sakkis

On Apr 28, 10:59 pm, "Gabriel Genellina"
def property_default(prop_name, default_value=None, doc=None):

     attr_name = '_'+prop_name

     def fget(self, attr_name=attr_name,
                    default_value=default_value):
         return getattr(self, attr_name, default_value)

     def fset(self, value,
                    attr_name=attr_name,
                    default_value=default_value):
         if value == default_value:
             delattr(self, attr_name)
         else:
             setattr(self, attr_name, value)

     return property(fget=fget, fset=fset, doc=doc)

When setting the same value as the default, the instance attribute is  
removed (so the default will be used when retrieving the value later). I  
think this is what you intended to do.

Note that this will fail if the value is already equal to the default
and you try to reset it to the default, so it needs an extra
hasattr(self, attr_name) before the delattr. Regardless, I would be
surprised with the following behaviour:
<type 'float'>

Another simpler alternative would be to (ab)use a decorator:

def defaultproperty(func):
attr = '_' + func.__name__
default = func.func_defaults[0]
return property(
fget = lambda self: getattr(self, attr, default),
fset = lambda self,value: setattr(self, attr, value),
doc = func.__doc__)


class Rectangle(object):
'''A beautiful Rectangle'''

@defaultproperty
def length(default=12.0):
'''This is the length property'''

George
 
G

Gabriel Genellina

En Tue, 29 Apr 2008 01:29:40 -0300, George Sakkis
On Apr 28, 10:59 pm, "Gabriel Genellina"


Note that this will fail if the value is already equal to the default
and you try to reset it to the default, so it needs an extra
hasattr(self, attr_name) before the delattr. Regardless, I would be
surprised with the following behaviour:

<type 'float'>

Yep, probably the best thing to do is to always call setattr and avoid
special cases.
Another simpler alternative would be to (ab)use a decorator:

def defaultproperty(func):
attr = '_' + func.__name__
default = func.func_defaults[0]
return property(
fget = lambda self: getattr(self, attr, default),
fset = lambda self,value: setattr(self, attr, value),
doc = func.__doc__)


class Rectangle(object):
'''A beautiful Rectangle'''

@defaultproperty
def length(default=12.0):
'''This is the length property'''


Nice, although that empty function looks somewhat strange...
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
474,262
Messages
2,571,043
Members
48,769
Latest member
Clifft

Latest Threads

Top