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.