Understanding default values for object database backed attributes

Author:Kevin Teague (kteague)
Version:unkown

Using class attributes as default values in Model objects is a common idiom, understand how this works.

Imagine you want to make a very simple Shoe Model class to define how you will manage data about Shoe objects. In Grok you would typically create an interface which described the data schema of a Shoe, and create a class that inherits from grok.Model that implements the Shoe data schema. While it’s not strictly necessary to declare the data schema of an object in Python, and it’s possible to dynamically add arbitrary data attributes to a object and store those in the Zope Object Database (ZODB), it’s generally good practice to be more formal with your data schemas and only deviate from pattern when supporting a specific use-case.

from zope.interface import Interface
from zope import schema
import grok

class IShoe(Interface):
    kind = schema.TextLine(
        title=u'Kind of Shoe',
        default=u'Sneaker',
    )

class ClassAttributeBasedShoe(grok.Model):
    grok.implements(IShoe)
    kind = u'Sneaker'

class ObjectAttributeBasedShoe(grok.Model):
    grok.implements(IShoe)

    def __init__(self, kind=u'Sneaker'):
        self.kind = kind

Elsewhere you would also define your Application and perhaps a separate Shoes Container for managing collections of Shoes. Then you could create and store shoes with:

class CreateSingleHardcodedShoeView(grok.View):
    grok.context(ShoesContainer)

    def render(self):
        new_shoe = ClassAttributeBasedShoe()
        self.context['test_shoe'] = new_shoe
        return """
        <html><h1>
        I saved a shoe of kind %s the object database!
        </h1></html>
        """ % new_shoe.kind

In the above example, your IShoe schema declared that shoe data objects have an attribute named kind which is a single line of text, and the default value is Sneaker. Interfaces are only abstract, formalized descriptions of an object - the IShoe interface only states that Shoe objects should supply the value Sneaker as a default. It’s up to the Model implementation class to actually handle the details of default values. We will look at two different ways of handling default values: using class attributes and using object attributes, as well as the subtle difference between these two implementations.

Class attributes and default values

In Python, class objects have attributes, and instance objects of a class have attributes. There is always only a single class object, but often you will have many instance objects of a given class. If you are familiar with working with data in a relational database, you can think of class objects as being analgous to a table, and instance objects as being analgous to rows within that table.

For a further primer on classes and objects, see the section on Classes in the Python Tutorial.

Normally, only instance objects are stored in an object database, class objects are only stored in your Python code. For example:

shoe = ClassAttributeBasedShoe()
return "<h1>The kind of shoe is %s</h1>" % shoe.kind

How does an object instance magically acquire a kind attribute in the above example? The answer is that object attributes are shadowed by class attributes. During normal instance object attribute look-up, if the object doesn’t directly provide an attribute, then the class object is checked for an attribute of that name - if the class has that attribute then it is used instead. If you are using an instance object, you can always get to the shadowed class attribute value by using the magic name of __class__. For example, try this on Python’s interactive interpreter:

>>> class FooBarShoe(object):
...     kind = u'Foo Bar'
...
>>> foo_shoe = FooBarShoe()
>>> foo_shoe.kind
u'Foo Bar'
>>> foo_shoe.kind = u'Extremely bling Foo Bar'
>>> foo_shoe.kind
u'Extremely bling Foo Bar'
>>> foo_shoe.__class__.kind
u'Foo Bar'

So what does this mean when we were using the ClassAttributeBasedShoe implementation to provide the default kind attribute for instance objects? Class attributes are not persisted in the ZODB! If you make twenty shoes, and they all rely on the default values, and save those twenty shoes in the database, then only fact that these objects are instances of the ClassAttributeBasedShoe is saved. The default value is not stored to disk for any of these objects. If you change the ClassAttributeBasedShoe to:

class ClassAttributeBasedShoe(grok.Model):
    grok.implements(IShoe)
    kind = u'Work boot'

Then when you retrieve all of your shoe objects, the default value will be changed for all of them.

Object attributes and default values

Using object attributes to store data, your objects will behave similar to a relational database. If you create and store a shoe object, then the default value for the kind attribute will be written to disk. If you later change the default value, then the kind attribute for existing objects will not change:

class ObjectAttributeBasedShoe(grok.Model):
    grok.implements(IShoe)

    def __init__(self, kind=u'Sneaker'):
        self.kind = kind

Which is better?

The answer is, “it depends.”

Using Class attributes for default values is more efficient than object attributes, since less data is written to disk. Creating an implementation using Class attributes is also more concise and slightly more readable. However, care is necessary with Class attributes, since changing this value will change that value for all objects derived from that class. Sometimes though, this is exactly what you want, if you are using class attributes for default values and want to update all existing objects to that default, it’s not necessary to write an migration code to update objects already stored in the ZODB.

Using object attributes for default values is going to increase the amount of data written to disk. It will make the class definition slighlty longer. However, if your goal is to permanently store the default value to disk for each object, then this is exactly what you want. For example, if you have an object which stores the version number of an program used to perform a data analysis run, then you definitely want to use object attributes, since if you used a default stored as a class attribute, older data analysis results performed with an older version number would be automagically updated to the latest value stored in the class attribute!

You can also mix-and-match class attributes and object attributes to support any specific requirements of your implementation. For example, if you like declaring defaults as class attributes, but want to copy that value into an object attribute during initialization you might write:

class ObjectAndClassShoe(grok.Model):
    grok.implements(IShoe)
    kind = u'Mountaineering boot'

    def __init__(self, kind=None):
        if not kind:
            kind = self.__class__.kind
        self.kind = kind