September 5, 2010

A prototypal binding trap

It always pains me to explain these little identifier resolution traps:

#!/usr/bin/env python3

class Egg:

    _next_id = 1

    def __init__(self):
        self.id = self._next_id
        self._next_id += 1
        assert Egg._next_id is self._next_id


if __name__ == '__main__':
    f = Egg()

Fails the assertion. It's decomposing the assignment-update into its constituent tmp = self._next_id + 1; self._next_id = tmp components, but a programmer could reasonably expect a Lookup/Update hash map ADT operation to occur instead — get the slot by lookup, mutate the value found, and store back in an atomic sense, clobbering the class member with the updated value — but that's not how it works.

This goes with the prototypal territory:

function Egg() {
    this.id = this.next_id;
    this.next_id += 1;
    assertEq(this.next_id, Egg.prototype.next_id);
}

Egg.prototype.next_id = 1;
var e = new Egg(); // Error: Assertion failed: got 2, expected 1

Fortunately, note that this behavior definitely has the semantics you want when you add inheritance to the mix. Is the self.viscosity that this class implementation is referring to a class member or an instance member in the base class?

#!/usr/bin/env python3

import sauce

class AwesomeSauce(sauce.Sauce):

    def __init__(self):
        super().__init__()
        self.viscosity += 12 # Deca-viscoses per milliliter.

The answer is that you don't care — it's being rebound on the AwesomeSauce instance no matter what.

Moral of the story is to be mindful when using update operations on class members — if you want to rebind a class member within an instance method, you've got to use the class name instead of self.