Custom Object Model

October 9th, 2006 by Bernard Lebel - Viewed 7293 times - Popularity: 11%




2. HAND DOWNS: OPERATOR OVERLOADING

Terminology
Before moving on, I need to clarify a few things. I’m going to discuss classes as well as some aspects of the XSI Object Model. In Python, the terminology used to describe certain features of classes can be confused with the terminology used in the XSI Object Model. Specifically, I’m talking about the word “Properties”.

In Python, “properties”, are attributes of classes that do not use the function notation to return a value. They are referred to as “data attributes” in the Python Tutorial, but more problematically as “property attributes” in some cases. In XSI, “properties” describe pretty much the same thing, but with one exception: the term Properties is also a named attribute that refers to any property object that can be found under an X3DObject, such as a cluster. Such property objects include the geometry approximation property, visibility property, custom properties, texture projection, and the like.

Since we will often refer to the XSI Object Model, I will not use the term “property” to talk about non-callable attributes. Instead, I will use “callable” for methods and callable objects, and “non-callable” for, well, non-callable class attributes.

The basics
Let’s start building the object model interface. The first step is to write a bunch of classes and implement operator overloading. To start with, let’s replicate some features of the XSI Object Model.

First, you need a base class. This class, let’s call it X3DObject. This is a polygon mesh object in the 3D scene. That class has a few non-callable attributes, like Name and Type.

1
2
3
4
class X3DObject:
    def __init__(self):
        self.name = "object"
        self.type = "polymsh"

One attribute, two behaviors
Many other non-callable attributes that we want to attach to this class have two distinct behaviors.

The first behavior is the ability to get a PropertyCollection instance when you read the “Properties” attribute of the X3DObject class. In this case, it seems like Properties is an non-callable attribute. The second behavior is that you get a single Property instance when you lookup the Properties attributes. When you do that, you use a string to name the Property you want. This time, it seems that “Properties” is behaving like a method attribute. Let’’s put this into code:

1
2
oProperties = X3DObject.Properties # looks like I'm reading a property attribute, this returns a PropertyCollection instance
oProperty = X3DObject.Properties("property name") # looks like I''m calling a method attribute, this returns a Property instance

If you try to implement “Properties” both as a non-callable attribute and a method attribute of the X3DObject class, you’ll run into some troubles. In Python, non-callable names override callable ones. So, is the attribute “Properties” a callable attribute, or is it a non-callable attribute? The answer lies between the two.

The first part of the solution is that “Properties” is actually a full-fledge class in itself. Let’s create a class for “Properties”. For now, this class will do nothing.

1
2
3
class PropertyCollection:
    def __init__(self):
        self.name = "Properties"

“Properties” as a non-callable attribute
We need a way to “bind” the PropertyCollection class with the X3DObject one. Another way to put it is to say that we need to decide by what means does the X3DObject refer to the PropertyCollection class. One way to do that is to have the PropertyCollection instantiated when the X3DObject is:

1
2
3
4
5
6
7
8
9
10
11
class PropertyCollection:
    def __init__(self):
        self.name = 'Properties'
 
class X3DObject:
    def __init__(self):
        self.name = 'object'
        self.type = 'polymsh'
 
        # Instantiate the PropertyCollection object
        self.Properties = PropertyCollection()

This is a totally valid approach. However, it has this limitation where PropertyCollection is instantiated immediately, without knowing anything about the state of the program. What I propose here is to “delay” that instantiation until it is really needed. To do that, we can use the __getattr__ operator overload. This method, when put into a class, will intercept attribute qualification for names not found in the instance. In other words, it allows us to take a decision “on-demand” for when the user is looking for a specific attribute not set on the instance. Let’s translate this idea into code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class X3DObject:
    def __init__(self):
        self.name = "object"
        self.type = "polymsh"
 
    def __getattr__(self, sAttribute):
        """
        Let''s intercept some of the attribute qualifications attempted on this instance.
        If these attributes match a set of predefined attributes,
        return the corresponding class instance.
        """
 
        if sAttribute == 'Properties':
             # User is using "Properties", let's give him an instance
             return PropertyCollection()
        else:
             return None

“Properties” as a callable attribute
We want the ability to return an object when we use a string in a sequence lookup, like this:

1
oProperty = X3DObject.Properties("property name")

For the Python novice, this may look very similar to looking up in a dictionary where classes would be mapped to string, like X3DObject.Properties["property name"]. While syntactically it looks very similar, it’s not exactly what’s happening. The first thing to remember is that in Python, when you use parentheses after a name, you are calling this name. So it’s not a dictionary lookup, but a call.

Thanks to the __call__ method, instances can be made callable. So if you put this method in your class, you can intercept calls made to it, and take the appropriate action, just like the __getattr__ we saw a moment ago. In this particular case, if the supplied argument is right, you would return a brand new instance of another class. So let’s implement the __call__ method in our PropertyCollection class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class PropertyCollection:
    def __init__(self):
        self.name = 'Properties'
 
    def __call__(self, sPropertyName):
        """
        Let's intercept the calls made to this instance.
        If the name used matches a set of predefined properties,
        return the corresponding class instance.
        """
 
        if sPropertyName == 'visibility':
            return PropertyObject(sPropertyName)
        else:
            return None

So here, if the property name I’m looking for is “visibility”, I return a PropertyObject instance. Let’s define this new class:

1
2
3
class PropertyObject:
    def __init__(self, sPropertyName):
        self.name = sPropertyName

So there you go. With this code, you could run this line of code:

1
2
oObject = X3DObject()
print oObject.Properties('visibility').name

The next step would be to define a ParameterCollection class, as well as a ParameterObject class. Get sample code here.

Pages: 1 2 3

Leave a Reply