Martijn Pieters' answer is as close as you're going to get to an answer useful for practical purposes (got my upvote), but I was interested in johnrsharpe's point about mutability. For instance, using Martijn's solution, the following fails:
a = Variable('x', 0)
b = Variable('x', 0)
c = Variable('y', 0)
a.letter = c.letter
assert(a is c)
We want equal instances to always refer to the same object in memory. This is very tricky, requires some black magic, and should never ever ever be used in a real application, but is in some sense possible. So, if you're in it for the laughs, come along for the ride.
My first thought was that we need to overload __setattr__ for Variable so that when an attribute changes, a new instance with the appropriate attribute values is created and all references (Footnote 1) to the original instance are updated to point to this new instance. This is possible with pyjack, but it turns out not to give us quite the right solution. If we do the following:
a = Variable('x', 0)
b = Variable('x', 0)
a.letter = 'y'
and in the process of that last assignment update all references to the object referred to as a
, then b
will also end up with b.letter == 'y'
since a
and b
(obviously) refer to the same instance.
So, it's not a matter of updating all references to the Variable instance. It's a matter of updating the one reference we just changed. That is to say, for the namespace in which the attribute assignment was called, we need to update the locals to point to the new instance. This is not straightforward, but here is a method that works with all tests I could come up with. Note that this code does not have so much of a code smell as a full-on corpse-in-the-closet-for-three-days code reek about it. Again, do not use it for anything serious:
import inspect
import dis
class MutableVariable(object):
__slots__ = ('letter', 'index') # Prevent access through __dict__
previously_created = {}
def __new__(cls, letter, index):
if (letter, index) in cls.previously_created:
return cls.previously_created[(letter, index)]
else:
return super().__new__(cls)
def __setattr__(self, name, value):
letter = self.letter
index = self.index
if name == "letter":
letter = value
elif name == "index":
index = int(value)
# Get bytecode for frame in which attribute assignment occurred
frame = inspect.currentframe()
bcode = dis.Bytecode(frame.f_back.f_code)
# Get index of last executed instruction
last_inst = frame.f_back.f_lasti
# Get locals dictionary from namespace in which assignment occurred
call_locals = frame.f_back.f_locals
assign_name = []
attribute_name = []
for instr in bcode:
if instr.offset > last_inst: # Only go to last executed instruction
break
if instr.opname == "POP_TOP": # Clear if popping stack
assign_name = []
attribute_name = []
elif instr.opname == "LOAD_NAME": # Keep track of name loading on stack
assign_name.append(instr.argrepr)
elif instr.opname == "LOAD_ATTR": # Keep track of attribute loading on stack
attribute_name.append(instr.argrepr)
last_instr = instr.opname # Opname of last executed instruction
try:
name_index = assign_name.index('setattr') + 1 # Check for setattr call
except ValueError:
if last_instr == 'STORE_ATTR': # Check for direct attr assignment
name_index = -1
else: # __setattr__ called directly
name_index = 0
assign_name = assign_name[name_index]
# Handle case where we are assigning to attribute of an attribute
try:
attributes = attribute_name[attribute_name.index(name) + 1: -1]
attribute_name = attribute_name[-1]
except (IndexError, ValueError):
attributes = []
if len(attributes):
obj = call_locals[assign_name]
for attribute_ in attributes:
obj = getattr(obj, attribute_)
setattr(obj, attribute_name, MutableVariable(letter, index))
else:
call_locals[assign_name] = MutableVariable(letter, index)
def __init__(self, letter, index):
super().__setattr__("letter", letter) # Use parent's setattr on instance initialization
super().__setattr__("index", index)
self.previously_created[(letter, index)] = self
def __str__(self):
return self.letter + '_' + str(self.index)
# And now to test it all out...
if __name__ == "__main__":
a = MutableVariable('x', 0)
b = MutableVariable('x', 0)
c = MutableVariable('y', 0)
assert(a == b)
assert(a is b)
assert(a != c)
assert(a is not c)
a.letter = c.letter
assert(a != b)
assert(a is not b)
assert(a == c)
assert(a is c)
setattr(a, 'letter', b.letter)
assert(a == b)
assert(a is b)
assert(a != c)
assert(a is not c)
a.__setattr__('letter', c.letter)
assert(a != b)
assert(a is not b)
assert(a == c)
assert(a is c)
def x():
pass
def y():
pass
def z():
pass
x.testz = z
x.testz.testy = y
x.testz.testy.testb = b
x.testz.testy.testb.letter = c.letter
assert(x.testz.testy.testb != b)
assert(x.testz.testy.testb is not b)
assert(x.testz.testy.testb == c)
assert(x.testz.testy.testb is c)
So, basically what we do here is use dis to analyze the bytecode for the frame in which the assignment occurred (as reported by inspect). Using this, we extract the name of the variable referencing the MutableVariable instance undergoing attribute assignment, and update the locals dictionary for the corresponding namespace so that that variable references a new MutableVariable instance. None of this is a good idea.
The code shown here is almost certainly implementation specific and may be the most fragile piece of code I've ever written, but it does work on standard CPython 3.5.2.
Footnote 1: Note that here, I am not using reference in the formal (e.g. C++) sense (since Python is not pass by reference) but in the sense of a variable referring to a particular object in memory. i.e. in the sense of "reference counting" not "pointers vs. references."