【问题标题】:force like instances of the same class to have the same referent强制同一类的类似实例具有相同的引用
【发布时间】:2017-06-28 09:57:43
【问题描述】:

有没有办法让用户定义的类像int 一样运行,因为任何相等的实例都具有相同的参照物?

例如:

>>> 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

【问题讨论】:

  • 在阅读了这个问题stackoverflow.com/questions/11611750/… 之后,我了解到这种行为取决于字符串的实现。不过,我不相信整数就是这种情况。
  • 你被 CPython 的实现细节的优化引入了歧途,即 small-int 缓存和字符串实习。
  • 特定于ints 的实现。事实上,它只适用于从-5256 的整数。同样,这是一种优化。请参阅this 问题。还有对文字的窥视孔优化。
  • 你想实现__new__。例如,请参阅stackoverflow.com/questions/674304/pythons-use-of-new-and-init
  • 因为任何相等的实例都有相同的参照物x = 600; y = 601 然后x is (y - 1)False。您误解了正在发生的事情。

标签: python reference


【解决方案1】:

有没有办法让用户定义的类像 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

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

【讨论】:

    【解决方案2】:

    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),而是在引用内存中特定对象的变量意义上。即在“引用计数”而不是“指针与引用”的意义上。

    【讨论】:

    • 哎呀!如果没有其他原因,这个答案很有帮助,它完全让我相信这是我不想做的事情:D 谢谢
    • 只是为了让它更吓人......睡在上面之后,我意识到我在测试中没有涵盖的另一件事几乎肯定会破坏这一点 - 如果正在更新的参考来自一些其他数据结构。例如。 mylist = [a] 然后mylist[0].letter = b.letter
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-05-08
    • 1970-01-01
    • 2020-03-03
    • 1970-01-01
    • 2013-11-13
    • 2018-11-09
    • 2020-02-18
    相关资源
    最近更新 更多