强迫同一个类的实例具有相同的指示

时间:2017-06-27 19:21:20

标签: python reference

有没有办法让用户定义的类像int那样运行,因为任何相等的实例都具有相同的指示对象?

E.g:

>>> a = 2
>>> b = 2
>>> a == b
True
>>> a is b
True

但是用户定义的类就像这样:

class Variable:
def __init__(self, letter, index):
    self.letter = letter
    self.index = int(index)

def __str__(self):
    return self.letter + '_' + str(self.index)

我们有以下内容:

>>> a = Variable('x',1)
>>> b = Variable('x',1)
>>> a == b
True
>>> a is b
False

2 个答案:

答案 0 :(得分:2)

  

有没有办法让用户定义的类像int一样操作,因为任何相等的实例都具有相同的指示对象?

首先,只有有限数量的整数表现得那样;出于性能和内存效率的原因,小整数被实现(参见"is" operator behaves unexpectedly with integers)。

您要求的是如何确保您自己的实例被实习,因为只有一个实例的副本可用于给定的值'。您可以通过控制何时创建新实例,通过实现自己的__new__ method

来实现
class Variable:
    _instances = {}

    def __new__(cls, letter, index):
        index = int(index)
        try:
            # return existing instance
            return cls._instances[letter, index]
        except KeyError:
            # no instance yet, create a new one
            instance = super().__new__(cls)
            instance._letter = letter
            instance._index = index
            cls._instances[letter, index] = instance
            return instance

    def __str__(self):
        return self._letter + '_' + str(self._index)

对于给定的letterindex组合,只创建一个实例:

>>> a = Variable('a', 1)
>>> b = Variable('a', 1)
>>> a
<__main__.Variable object at 0x10858ceb8>
>>> b
<__main__.Variable object at 0x10858ceb8>
>>> a is b
True

这实质上是整数实习的工作方式。

答案 1 :(得分:1)

Martijn Pieters&#39;答案就像你接近得到一个对实际目的有用的答案(得到我的upvote),但我对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'结束ab(显然)指的是同一个实例。

因此,它不是更新对Variable实例的所有引用的问题。这是更新我们刚刚更改的一个引用的问题。也就是说,对于调用属性赋值的命名空间,我们需要更新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),而是在引用内存中特定对象的变量意义上。即,&#34;引用计数&#34;不是&#34;指针与参考。&#34;