The Occasional Occurence

Odd Old-Style vs. New-Style Class Behavior

May 21, 2009 at 10:55 AM | categories: Python, Software, work, computing, General

So we have some older Python code at work that uses old-style classes. We usually try and bring those up to date when we encounter them.

The other day one of the developers did that and one of our tests started failing. A simple change from:

class Foo:
    # stuff here

to:

class Foo(object):
    # stuff here

was all that happened.

Here is some code that encapsulates the problem and runs with some interesting results:

"""Two nearly identical classes with quite different behavior."""

class LameContainerOld:
    def __init__(self):
        self._items = {'bar':'test'}

    def __getitem__(self, name):
        return self._items[name]

    def __getattr__(self, attr):
        return getattr(self._items, attr)

class LameContainerNew(object):
    def __init__(self):
        self._items = {'bar':'test'}

    def __getitem__(self, name):
        return self._items[name]

    def __getattr__(self, attr):
        return getattr(self._items, attr)

if __name__ == '__main__':
    for cls in [LameContainerOld, LameContainerNew]:
        container = cls()
        print "Testing", cls
        try:
            'foo' in container
        except Exception, e:
            print "\tMembership in %s raised %r!" % (container.__class__, e)
        else:
            print "\tMembership in %s worked!" % container.__class__
        print "\t%s" % container.__getitem__

Here is some output from running that:

Testing __main__.LameContainerOld
        Membership in __main__.LameContainerOld worked!
            bound method LameContainerOld.__getitem__ of {'bar': 'test'}
Testing     class '__main__.LameContainerNew'
        Membership in     class '__main__.LameContainerNew'     raised KeyError(0,)!
            bound method LameContainerNew.__getitem__ of     __main__.LameContainerNew object at 0xb7d1c1ac

From what I can tell, when a membership test happens on the old-style instance, a membership test is done on self._items and it returns False. When the membership test happens on the new-style instance, it tries to treat it like a sequence and calls the __getitem__ method with index 0.

Does that seem like a correct analysis? Does anyone know why the behavior is different there?

Also look at the output for printing container.__getitem__. Isn't __getattr__ only supposed to be called when the attribute isn't present on the instance? Why does it return the __getitem__ method of self._items for the old-style instance then?

Very puzzling.

cw