The Cobj (short for "context object") system is a Python & Java micro-framework for building small, tightly-coupled assemblages of objects that share a lot of information. In essence it's the magic associated with Field's _self variable re-factored out into pure Jython with an eye towards "dependency injection" like problems. We've come across situations that are well treated with this kind of approach in a whole variety of domains:
Field's _self
framework comes from spotting this shared pattern, the Cobj
system is a tiny (a single Java class file) reimplementation and refactoring of it that enables broader use of it.
All the code for this page (together with its UnitTesting) can be found here: tutorial_cobj.field.zip.
Cobj
is a dict
The best way to understand this is just to see it in action. First, an import:
# this imports the base class
from field.context.Context import Cobj
Now we are free to make and assemble instances of Cobj
:
a = Cobj(name="a")
print a.name
#->a
A Cobj
is a lot like an (open) object in Python, which in turn is a lot like a Dict
. Anything can be the value of a property, and properties are created on demand
a.position = Vector3(1,2,3)
Access to missing properties is allowed — missing properties simply 'None'
print a.missing
#->None
This allows this pattern to access properties with default values:
something = a.missing or Vector3(1,2,3)
And this pattern to initialise things once:
a.wasMissing = a.wasMissing or Vector3(1,2,3)
print a.position
#->(1.0, 2.0, 3.0)
Finally, properties can be deleted using 'del':
del a.position
print a.position
#->None
So far, so boring. Now we get to the secret sauce
a = Cobj(name="a")
b = Cobj(name="b")
c = Cobj(name="c")
a.something = b
b.somethingElse = c
a.position = Vector3(1,2,3)
print a.something
But now "position" is visible in 'b' and 'c'
print c.position
#->(1.0, 2.0, 3.0)
print b.position
#->(1.0, 2.0, 3.0)
What is happening? Properties are resolved upwards (by depth first search):
b.position = Vector3(4,3,2)
print b.position
#->(4.0, 3.0, 2.0)
print c.position
#->(4.0, 3.0, 2.0)
del b.position
print c.position
#->(1.0, 2.0, 3.0)
This is the crux of the Cobj
— a dynamically assembled tree of instances that delegate upwards to resolve properties.
Sometimes you need to see behind the curtain — where are you getting that property from? Prefixing a property name with "where_" tells you where it is being gotten from:
print a == c.where_position
#->True
This yields this pattern
c.where_position=Vector3(6,5,4)
print a.position
#->(6.0, 5.0, 4.0)
Which is sugar for
c.where_position.position=Vector3(6,5,4)
Subclassing in Python works exactly as expected — "self" gets all of the magic:
a = Cobj(masterValue=Vector3(1,2,3))
class B(Cobj):
def doSomething(self):
print self.masterValue
a.something = B()
a.something.doSomething()
#->(1.0, 2.0, 3.0)
But, if you'd rather not have every object in the tree see all of the properties of your objects, you can still hide some of them:
a = Cobj(masterValue=Vector3(1,2,3))
class B(Cobj):
def __init__(self):
# but you can still hide things from the tree if you like
self.__dict__["hidden"]=3
def doSomething(self):
print self.masterValue
print self.hidden
a.something = B()
a.something.somethingElse = Cobj()
print a.something.somethingElse.hidden
#->None
computed_
"First let's build a hierarchy:
a = Cobj(name="a")
a.elements = [Cobj(n=n) for n in range(0, 10)]
a.elements[3].furthermore = [Cobj(n=n*n) for n in range(10, 20)]
And set a "default" on the whole tree
a.position = Vector3(1,2,3)
As you might expect:
print a.elements[3].furthermore[2].position
#->(1.0, 2.0, 3.0)
But now we can add a "computed" property:
a.elements[3].furthermore[2].compute_position = lambda at,start : at.parent().position*2
print a.elements[3].furthermore[2].position
#->(2.0, 4.0, 6.0)
This function is called whenever 'position' looked up at this level of the tree — two parameters are passed in: the first is the location where we currently are in the tree (where this function is stored) the second is the place where this lookup started.
Clearly, Cobj do something special when properties have Cobj values. But they also understand collections:
a = Cobj(name="a")
a.elements = [Cobj(n=n) for n in range(0, 10)]
This gets "name" from the common parent 'a'
print a.elements[3].name
#->a
down_
Usually property lookup is about traversing upwards through the hierarchy, but sometimes its useful to query this tree from top to bottom:
a = Cobj(name="a")
a.elements = [Cobj(n=n) for n in range(0, 10)]
print a.down_n
#->[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Properties prefixed with "down_" collect results over the tree (in depth first order). For example:
a.elements[3].furthermore = [Cobj(n=n*n) for n in range(10, 20)]
print a.down_n
#->[0, 1, 2, 3, 100, 121, 144, 169, 196, 225, 256, 289, 324, 361, 4, 5, 6, 7, 8, 9]
There's also findAll, this runs a depth first predicate over the whole tree:
print [x.n for x in a.findAll(lambda x : x.n>4)]
#->[100, 121, 144, 169, 196, 225, 256, 289, 324, 361, 5, 6, 7, 8, 9]
And, finally, there's .visit
:
a = Cobj(name="a")
b = Cobj(name="b")
a.something = b
# this pattern can be useful to visit everything in a tree
def visitor(x):
print x
a.depthFirst(visitor)
Cobj trees can masquerade as objects that implement Java interfaces. For this example we'll actually need a Java interface. We could get this from anywhere --- a Field built in class, a .jar from somebody else that we are using, or something we define this in Eclipse --- it just needs to be visible to Field.
Alternatively we could just make one in Field's embedded Java compiler:
For this to work you need to turn on the JavaC language support in the "lambda" tab in the inspector. With this defined we now have a Java interface.
print AnInterface
#-><type 'AnInterface'>
Python can use this. Here we make a subclass of Cobj
that also implements AnInterface
:
class MyCObj(Cobj, AnInterface):
def doIt(self):
print "I'm doing it! (%s)" % self.name
a = MyCObj(name="a")
b = MyCObj(name="b")
Obviously, we can just call methods on these things
a.doIt()
#->I'm doing it! (a)
a.something = b
Bt this syntax gets us an implementation of AnInterface
that calls (depth first) everything in the hierarchy that implements AnInterface
everything = a.proxy(AnInterface)
everything.doIt()
# prints "I'm doing it! (a)" "I'm doing it! (b)"
Likewise:
a.somethingElse = [MyCObj(name=n) for n in range(0, 10)]
Having something that isn't AnInterface
hanging out in the tree is not a problem:
a.notAnInterface = Cobj(name="neverCalled")
Note, we don't have to regenerate the proxy 'everything' when we change the tree.
everything.doIt()
# this runs a,b, and 0-9
These proxy objects can be passed around to any Java method that's expecting an AnInterface
.
Finally an optional, slightly more advanced feature — the underscore object.
from field.context.Context import _
This underscore allows you to refer to the currently constructed object while you are constructing it. Thus:
a = Cobj(root=_)
Is shorthand for:
a = Cobj()
a.root = a
This is especially useful when constructing lists using list comprehensions
[Cobj(datum=_, value=n) for n in range(0, 10)]
We end up doing this occasionally when we do dependency "injection" like things with this framework:
a = Cobj(root=_, masterValue=Vector3(1,2,3))
class B(Cobj):
def doSomething(self):
# here we lookup where to get values from using the name 'root'
print self.root.masterValue
# this is an extra level of flexibility from just saying self.masterValue
a.something = B()
a.something.doSomething()
#->(1.0, 2.0, 3.0)