Martijn Pieters' 的答案与您将获得对实际目的有用的答案一样接近(得到我的赞成),但我对johnrsharpe 关于可变性的观点很感兴趣。例如,使用 Martijn 的解决方案,以下失败:
a = Variable('x', 0)
b = Variable('x', 0)
c = Variable('y', 0)
a.letter = c.letter
assert(a is c)
我们希望相等的实例总是引用内存中的同一个对象。这非常很棘手,需要一些黑魔法,并且应该永远在实际应用程序中使用,但在某种意义上是可能的。所以,如果你喜欢笑,那就来吧。
我的第一个想法是我们需要为变量重载 __setattr__ 以便当属性更改时,会创建一个具有适当属性值的新实例,并且对原始实例的所有引用(脚注 1)都会更新为指向这个新实例实例。 pyjack 可以做到这一点,但事实证明这并没有给我们提供完全正确的解决方案。如果我们执行以下操作:
a = Variable('x', 0)
b = Variable('x', 0)
a.letter = 'y'
并且在最后一次分配更新的过程中所有对称为a的对象的引用,然后b也将以b.letter == 'y'结束,因为a和@987654333 @(显然)指的是同一个实例。
因此,更新对变量实例的所有 引用不是问题。这是一个更新我们刚刚更改的引用的问题。也就是说,对于调用属性赋值的命名空间,我们需要更新locals指向新的实例。这并不简单,但这是一种适用于我能想到的所有测试的方法。请注意,此代码没有那么多代码气味,而是一个完整的 尸体在壁橱里的三天代码恶臭。同样,不要将它用于任何严重的事情:
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)
所以,基本上我们在这里所做的是使用dis 来分析发生分配的帧的字节码(由inspect 报告)。使用它,我们提取引用正在分配属性的 MutableVariable 实例的变量的名称,并更新相应命名空间的 locals 字典,以便该变量引用新的 MutableVariable 实例。这些都不是一个好主意。
这里显示的代码几乎肯定是特定于实现的,可能是我写过的最脆弱的一段代码,但它确实适用于标准 CPython 3.5.2。
脚注 1:请注意,在这里,我不是在正式(例如 C++)意义上使用引用(因为 Python is not pass by reference),而是在引用内存中特定对象的变量意义上。即在“引用计数”而不是“指针与引用”的意义上。