Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

descriptor & docstring

2 views
Skip to first unread message

cyril giraudon

unread,
Apr 28, 2008, 1:35:40 PM4/28/08
to
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.)

cyril giraudon

unread,
Apr 28, 2008, 4:53:26 PM4/28/08
to
A precision, I use python 2.5.2 under linux mandiva 2007.0

Cyril.


Gabriel Genellina

unread,
Apr 28, 2008, 10:59:59 PM4/28/08
to pytho...@python.org
En Mon, 28 Apr 2008 14:35:40 -0300, cyril giraudon
<cyril.g...@gmail.com> escribió:

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

--
Gabriel Genellina

George Sakkis

unread,
Apr 29, 2008, 12:29:40 AM4/29/08
to
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:

>>> r = Rectangle()
>>> r.length = 4
>>> type(r.length)
<type 'int'>
>>> r.length = 12
>>> type(r.length)
<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

Gabriel Genellina

unread,
Apr 29, 2008, 2:43:05 AM4/29/08
to pytho...@python.org
En Tue, 29 Apr 2008 01:29:40 -0300, George Sakkis
<george...@gmail.com> escribió:

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

--
Gabriel Genellina

0 new messages