descriptor & docstring

Gabriel Genellina gagsl-py2 at yahoo.com.ar
Tue Apr 29 04:59:59 CEST 2008


En Mon, 28 Apr 2008 14:35:40 -0300, cyril giraudon  
<cyril.giraudon at 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




More information about the Python-list mailing list