I promised I'd put together a PEP for a 'generic object' data type for
Python 2.5 that allows one to replace __getitem__ style access with
dotted-attribute style access (without declaring another class). Any
comments would be appreciated!
Thanks!
Steve
----------------------------------------------------------------------
Title: Generic Object Data Type
Version: $Revision: 1.0 $
Last-Modified: $Date: 2004/11/29 16:00:00 $
Author: Steven Bethard <[EMAIL PROTECTED]>
Status: Draft
Type: Standards Track
Content-Type: text/x-rst
Created: 29-Nov-2004
Python-Version: 2.5
Post-History: 29-Nov-2004
Abstract
========
This PEP proposes a standard library addition to support the simple
creation of 'generic' objects which can be given named attributes
without the need to declare a class. Such attribute-value mappings are
intended to complement the name-value mappings provided by Python's
builtin dict objects.
Motivation
==========
Python's dict objects provide a simple way of creating anonymous
name-value mappings. These mappings use the __getitem__ protocol to
access the value associated with a name, so that code generally appears
like::
mapping['name']
Occasionally, a programmer may decide that dotted-attribute style access
is more appropriate to the domain than __getitem__ style access, and
that their mapping should be accessed like::
mapping.name
Currently, if a Python programmer makes this design decision, they are
forced to declare a new class, and then build instances of this class.
When no methods are to be associated with the attribute-value mappings,
declaring a new class can be overkill. This PEP proposes adding a
simple type to the standard library that can be used to build such
attribute-value mappings.
Providing such a type allows the Python programmer to determine which
type of mapping is most appropriate to their domain and apply this
choice with minimal effort. Some of the suggested uses include:
Returning Named Results
-----------------------
It is often appropriate for a function that returns multiple items to
give names to the different items returned. The type suggested in this
PEP provides a simple means of doing this that allows the returned
values to be accessed in the usual attribute-style access::
>>> def f(x):
... return Bunch(double=2*x, squared=x**2)
...
>>> y = f(10)
>>> y.double
20
>>> y.squared
100
Representing Hierarchical Data
------------------------------
The type suggested in this PEP also allows a simple means of
representing hierarchical data that allows attribute-style access::
>>> x = Bunch(spam=Bunch(rabbit=1, badger=[2, 3, 4]), ham='neewom')
>>> x.spam.badger
[2, 3, 4]
>>> x.ham
'neewom'
Rationale
=========
As Bunch objects are intended primarily to replace simple classes,
simple Bunch construction was a primary concern. As such, the Bunch
constructor supports creation from keyword arguments, dicts, and
sequences of (attribute, value) pairs::
>>> Bunch(eggs=1, spam=2, ham=3)
Bunch(eggs=1, ham=3, spam=2)
>>> Bunch({'eggs':1, 'spam':2, 'ham':3})
Bunch(eggs=1, ham=3, spam=2)
>>> Bunch([('eggs',1), ('spam',2), ('ham',3)])
Bunch(eggs=1, ham=3, spam=2)
To allow attribute-value mappings to be easily combined, the update
method of Bunch objects supports similar arguments.
If Bunch objects are used to represent hierarchical data, comparison of
such objects becomes a concern. For this reason, Bunch objects support
object equality::
>>> x = Bunch(parrot=Bunch(lumberjack=True, spam=42), peng='shrub')
>>> y = Bunch(peng='shrub', parrot=Bunch(spam=42, lumberjack=True))
>>> z = Bunch(parrot=Bunch(lumberjack=True), peng='shrub')
>>> x == y
True
>>> x == z
False
Additionally, to allow users of the Bunch type to convert other
hierarchical data into Bunch objects, a frommapping classmethod is
supported. This can be used, for example, to convert an XML DOM tree
into a tree of nested Bunch objects::
>>> import xml.dom.minidom
>>> def getitems(element):
... if not isinstance(element, xml.dom.minidom.Element):
... raise TypeError('items only retrievable from Elements')
... if element.attributes:
... for key, value in element.attributes.items():
... yield key, value
... children = {}
... for child in element.childNodes:
... if child.nodeType == xml.dom.minidom.Node.TEXT_NODE:
... text_list = children.setdefault('text', [])
... text_list.append(child.nodeValue)
... else:
... children.setdefault(child.nodeName, []).append(
... Bunch.frommapping(child, getitems=getitems))
... for name, child_list in children.items():
... yield name, child_list
...
>>> doc = xml.dom.minidom.parseString("""\
... <xml>
... <a attr_a="1">
... a text 1
... <b attr_b="2" />
... <b attr_b="3"> b text </b>
... a text 2
... </a>
... <c attr_c="4"> c text </c>
... </xml>""")
>>> b = Bunch.frommapping(doc.documentElement, getitems=getitems)
>>> b.a[0].b[1]
Bunch(attr_b=u'3', text=[u' b text '])
Note that support for the various mapping methods, e.g.
__(get|set|del)item__, __len__, __iter__, __contains__, items, keys,
values, etc. was intentionally omitted as these methods did not seem to
be necessary for the core uses of an attribute-value mapping. If such
methods are truly necessary for a given use case, this may suggest that
a dict object is a more appropriate type for that use.
Reference Implementation
========================
(This will be replaced with a link to a SF patch when I think I've
made all the necessary corrections)::
import operator as _operator
class Bunch(object):
"""Bunch([bunch|dict|seq], **kwds) -> new bunch with specified
attributes
The new Bunch object's attributes are initialized from (if
provided) either another Bunch object's attributes, a
dictionary, or a sequence of (name, value) pairs, then from the
name=value pairs in the keyword argument list.
Example Usage:
>>> Bunch(eggs=1, spam=2, ham=3)
Bunch(eggs=1, ham=3, spam=2)
>>> Bunch({'eggs':1, 'spam':2, 'ham':3})
Bunch(eggs=1, ham=3, spam=2)
>>> Bunch([('eggs',1), ('spam',2), ('ham',3)])
Bunch(eggs=1, ham=3, spam=2)
>>> Bunch(Bunch(eggs=1, spam=2), ham=3)
Bunch(eggs=1, ham=3, spam=2)
"""
def __init__(self, *args, **kwds):
"""Initializes a Bunch instance."""
self.update(*args, **kwds)
def __eq__(self, other):
"""x.__eq__(y) <==> x == y"""
return (isinstance(other, self.__class__)
and self.__dict__ == other.__dict__)
def __repr__(self):
"""x.__repr__() <==> repr(x)
If all attribute values in this bunch (and any nested
bunches) are reproducable with eval(repr(x)), then the Bunch
object is also reproducable for eval(repr(x)).
"""
return '%s(%s)' % (self.__class__.__name__,
', '.join('%s=%r' % (k, v)
for k, v
in self.__dict__.items()))
def update(self, *args, **kwds):
"""update([bunch|dict|seq], **kwds) -> None
Updates a Bunch object's attributes from (if provided)
either another Bunch object's attributes, a dictionary, or a
sequence of (name, value) pairs, then from the name=value
pairs in the keyword argument list.
"""
if len(args) == 1:
other, = args
if isinstance(other, self.__class__):
other = other.__dict__
try:
self.__dict__.update(other)
except TypeError:
raise TypeError('cannot update Bunch with %s' %
type(other).__name__)
elif len(args) != 0:
raise TypeError('expected 1 argument, got %i' %
len(args))
self.__dict__.update(kwds)
@classmethod
def frommapping(cls, mapping, getitems=None):
"""Create a Bunch object from a (possibly nested) mapping.
Note that, unlike the Bunch constructor, frommapping
recursively converts all mappings to bunches.
Example Usage:
>>> Bunch.frommapping({'eggs':1,
... 'spam':{'ham':2, 'badger':3}})
Bunch(eggs=1, spam=Bunch(ham=2, badger=3))
Keyword Arguments:
mapping -- a mapping object
getitems -- a function that takes the mapping as a parameter
and returns an iterable of (key, value) pairs. If not
provided, the items method on the mapping object will be
used, or (key, mapping[key]) values will be generated if
the mapping object does not provide an items method.
Note that getitems will be applied recursively to each value
in the mapping. It should raise a TypeError if it is
applied to an object for which it cannot produce
(key, value) pairs.
"""
# determine which items() method to use
if getitems is None:
try:
getitems = type(mapping).items
except AttributeError:
getitems = _items
# build the Bunch from the mapping, recursively
result = cls()
for key, value in getitems(mapping):
try:
value = cls.frommapping(value, getitems=getitems)
except TypeError:
pass
setattr(result, key, value)
return result
def _items(mapping):
"""Produces (key, value) pairs from a mapping object.
Intended for use with mapping objects that do not supply an
items method.
"""
for key in mapping:
yield key, mapping[key]
Open Issues
===========
What should the type be named? Some suggestions include 'Bunch',
'Record' and 'Struct'.
Where should the type be placed? The current suggestion is the
collections module.
References
==========
..
Local Variables:
mode: indented-text
indent-tabs-mode: nil
sentence-end-double-space: t
fill-column: 70
End:
--
http://mail.python.org/mailman/listinfo/python-list