A prototypal binding trap
Sunday, September 5th, 2010It 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.