I wrote what I think is a nifty piece of code, and thought I'd share it, and get some feedback. The problem I was struggling with was that I wanted a flexible container object that other developers could use for storage and retrieval of objects, without pre-defining the structure.
In other words, I wanted it to be possible to do something like this:
if o.chatperson.hometown.country == "Mexico":
o.chatperson.native_language = "Spanish"
else:
o.chatperson.native_language = "English"
The code above examines the country of the hometown of the person that Amy Iris is chatting with, and if it's Mexico, then set their native language to Spanish, otherwise it sets it to English.
So far, not hard. The catch was that I wanted to be able to create and query the objects and properties on the fly. Using typical Python classes, the "if" statement above would raise an error, unless you have o defined, and o has a property of chatperson, and o.chatperson has a property of hometown, and o.chatperson.hometown has a property of country. I'd rather it not raise an error, but instead, create the hierarchy of sub-properties as needed to complete the request.
My goal was to create a general purpose container object that would create sub-containers on the fly, so that such a query wouldn't fail, and more importantly, it would allow me to set "deeply nested" properties, without having to pre-define the entire hierarchical structure.
So if I define this class properly, and set up an instance o, I should be able to execute the line:
o.chatperson.native_language = "English"
without having to define o.chatperson in advance.
I want this statement to automatically add a property called chatperson if one doesn't already exist, and make it a container to hold the native_language property.
By setting up a container class (I called it "Box"), and having "o" an instance of that class, I should be able to execute that line of code. Upon execution, "o" will create the container "chatperson", which will allow you to set the property called native_language. All auto-magically.
I called my class a "Box" class (looking for a shorter name than Container). The other features that I wanted were:
- properties and sub-properties would automatically be set up (as instances of Box), if they were queried or needed for an assignment.
- I wanted to track all changes to any instance of the class at the highest level. So setting o.chatperson.native_language to "English" will also log that change into a list called o._changelog.
- I wanted to be able to set and get properties by either the dotted notation (in an unquoted object name in my source code), or by string. So the property that I mentioned above could be accessed in a number of ways:
o.chatperson.hometown.country
o.chatperson.hometown.get_dotted("country")
o.chatperson.get_dotted("hometown.country")
o.get_dotted("chatperson.hometown.country")
o.chatperson.get_dotted("hometown").country
o.get_dotted("chatperson.hometown").country
o.get_dotted("chatperson").hometown.country
o.get_dotted("chatperson").hometown.get_dotted("country")
Likewise, o.set_dotted could be used to "deep set" a property (and all necessary parent container properties would automatically be built as needed). And o.del_dotted could be used to delete using a string to navigate down the object heirarchy.
So here's what I came up with. Feel free to use it or critique it:
class Box:
"""
Containers for Properties. This class will allow you to create instances
that contain properties and sub-properties.
It has an auto-create feature, so that if you use dotted notation and
reference properties and sub-properties that do not yet exist,
they are created automatically.
And you can create dotted-notation properties using variable names.
All changes are tracked in a _changelog list.
Examples:
>>> p1=Box(name="Chris",gender="male")
>>> p1
{'gender': 'male', 'name': 'Chris'}
>>> p2=Box(name="Joe",hometown="Cincinnati")
>>> p2
{'hometown': 'Cincinnati', 'name': 'Joe'}
>>> PeopleBox=Box(people=[p1,p2])
>>> PeopleBox
{'people': [{'gender': 'male', 'name': 'Chris'}, {'hometown': 'Cincinnati', 'name': 'Joe'}]}
>>> p2.contact_info.phone.home="513-555-1111" #auto-create contact_info.phone
>>> p2
{'hometown': 'Cincinnati', 'name': 'Joe', 'contact_info': {'phone': {'home': '513-555-1111'}}}
>>> p1.family_members=[p2,] #support list types
>>> p1
{'gender': 'male', 'name': 'Chris', 'family_members': [{'hometown': 'Cincinnati', 'name': 'Joe', 'contact_info': {'phone': {'home': '513-555-1111'}}}]}
>>> p1.family_members[0].contact_info.phone.mobile #access p2 through p2
{}
>>> p2.contact_info.phone.mobile="513-555-2222"
>>> p2
{'hometown': 'Cincinnati', 'name': 'Joe', 'contact_info': {'phone': {'mobile': '513-555-2222', 'home': '513-555-1111'}}}
>>> p1.family_members[0].contact_info.phone.mobile #access p2 through p1
'513-555-2222'
>>> p2.contact_info.phone.mobile='513-555-3333'
>>> p1.family_members[0].contact_info.phone.mobile #changes to p2 affect p1
'513-555-3333'
>>> properties="job.salary"
>>> p1.set_dotted(properties,120000) #can create properties whose dotted names are in an object
>>> p1.job.salary
120000
>>> p2._changelog #can see list of changes
[['setattr', 'contact_info.phone.home'], ['setattr', 'contact_info.phone.mobile'], ['setattr', 'contact_info.phone.mobile']]
>>> p2._all() #can see entire structure
{'name': 'Joe', 'contact_info': {'_top': '<pointer>', 'phone': {'_top': '<pointer>', 'home': '513-555-1111', 'mobile': '513-555-3333', '_name': 'contact_info.phone'}, '_name': 'contact_info'}, '_top': '<pointer>', 'hometown': 'Cincinnati', '_name': '', '_changelog': [['setattr', 'contact_info.phone.home'], ['setattr', 'contact_info.phone.mobile'], ['setattr', 'contact_info.phone.mobile']]}
"""
def __init__(self, **kwargs):
"""
Save any dictionary of keywords passed, into the container
"""
self.__dict__ = kwargs
_top=self.__dict__.setdefault("_top",self)
if id(_top) == id(self):
self.__dict__["_changelog"]=[]
self.__dict__["_name"]=""
def set_dotted(self,name,value):
"""
set a value, by passing a dot-notation property name and value
"""
parts=name.split(".",1)
if self.__dict__.get(parts[0],None).__class__ != Box:
_top = self.__dict__.get("_top",self)
_name = self._get_full_name(parts[0])
self.__dict__[parts[0]]=Box(_top=_top, _name=_name)
if len(parts)==1:
self.__setattr__(name, value)
else:
self.__dict__[parts[0]].set_dotted(parts[1],value)
def del_dotted(self,name):
"""
delete a property, by passing a dot-notation property name
"""
parts=name.split(".",1)
if len(parts)==1:
self.__delattr__(name)
else:
self.__dict__[parts[0]].del_dotted(parts[1])
def get_dotted(self,name):
"""
get a property, by passing a dot-notation property name
"""
parts=name.split(".",1)
if len(parts)==1:
return self.__dict__[name]
else:
return self.__dict__[parts[0]].get_dotted(parts[1])
def __getattr__(self, name):
if name[:2]!="__" or name[-2:]!="__": # skip the magic python ones.
_top = self.__dict__.get("_top",self)
_name = self._get_full_name(name)
self.__dict__[name]=Box(_top=_top, _name=_name) #auto-create
return self.__dict__[name]
else:
raise AttributeError, name
def __setattr__(self, name, value):
self.__dict__[name]=value
_name = self._get_full_name(name)
self.__dict__["_top"].__dict__["_changelog"].append(["setattr",_name,value])
def __delattr__(self, name):
del self.__dict__[name]
_name = self._get_full_name(name)
self.__dict__["_top"].__dict__["_changelog"].append(["delattr",_name])
def _get_full_name(self, name):
"""
determine full dot-notation name of the property
"""
_name = [self.__dict__["_name"]]
if len(_name[0])==0:
_name=[]
_name.append(name)
_name=".".join(_name)
return _name
def __repr__(self):
"""
simple representation, no hidden values
"""
temp={}
for (k,v) in self.__dict__.items():
if k not in ["_name","_changelog","_top"]:
temp[k]=v
return repr(temp)
def _all(self):
"""
return all values including hidden ones
"""
temp={}
for (k,v) in self.__dict__.items():
if k == "_top":
temp[k]="<pointer>"
elif v.__class__ == Box:
temp[k] = v._all()
else:
temp[k] = v
return temp

2 comments:
That looks a lot like a dingus: http://pypi.python.org/pypi/dingus/ where you can say: a.b.c.d.e and it creates the objects as it goes along.
You can do other things to dingus objects as well, so you might not want all of it, but there might be some common ground.
D'gou:
Thanks for tip on "dingus".
Definitely looks like something similar. I'm always amazed at all the Python code that's out there... so much to learn from!
Post a Comment