Python的写时复制行为

8

我正在解决一个问题,其中我正在实例化许多对象。大多数情况下,实例化的对象是相同的。为了减少内存开销,我希望所有相同的对象指向相同的地址。然而,当我修改对象时,我希望创建一个新的实例--基本上是写时复制行为。在Python中,最好的方法是什么?

享元模式接近于这个问题。下面是一个示例(来自http://codesnipers.com/?q=python-flyweights):

import weakref

class Card(object):
    _CardPool = weakref.WeakValueDictionary()
    def __new__(cls, value, suit):
        obj = Card._CardPool.get(value + suit, None)
        if not obj:
            obj = object.__new__(cls)
            Card._CardPool[value + suit] = obj
            obj.value, obj.suit = value, suit
        return obj

这个的行为如下所示:
>>> c1 = Card('10', 'd')
>>> c2 = Card('10', 'd')
>>> id(c1) == id(c2)
True
>>> c2.suit = 's'
>>> c1.suit
's'
>>> id(c1) == id(c2)
True

期望的行为应该是:
>>> c1 = Card('10', 'd')
>>> c2 = Card('10', 'd')
>>> id(c1) == id(c2)
True
>>> c2.suit = 's'
>>> c1.suit
'd'
>>> id(c1) == id(c2)
False

更新:我发现了享元模式,它似乎几乎符合要求。但是,我也愿意尝试其他方法。


我认为用你的确切示例不可能实现。c1c2是同一个对象。当您在其中一个上设置属性时,没有办法使其变成另一个对象,除非您获取一个新实例并让类给您提供一个新副本。这可能涉及到一种稍微不同的方法,涉及大量的__setattr__魔法。 - jdi
1
使用一个包装器来封装卡片。读操作查看当前的卡片,写操作将更改所引用的卡片。要使语法符合您的要求,您需要处理许多操作。我能想到的最接近的类比是您正在尝试实现指向指针的指针。 - Stephen Garle
@jdi 我就猜到了。有人做过这个(或类似的)吗?有可用的示例吗? - DMack
也许你可以使用更高效的数据结构,例如紧凑整数数组或内部化字符串。 - Keith
3个回答

7
你是否需要id(c1)==id(c2)是相同的,还是这只是一个演示,真正的目标是避免创建重复的对象?
一种方法是使每个对象都是独特的,但保持对“真实”对象的内部引用,就像你上面所做的那样。然后,在任何__setattr__调用上,更改内部引用。
我以前从未做过__setattr__的事情,但我认为它看起来像这样:
class MyObj:
    def __init__(self, value, suit):
        self._internal = Card(value, suit)

    def __setattr__(self, name, new_value):
        if name == 'suit':
            self._internal = Card(value, new_value)
        else:
            self._internal = Card(new_value, suit)

同样地,通过getattr公开属性。

你仍然会有很多重复的对象,但它们背后只有一个'真正的'备份对象的副本。因此,如果每个对象都非常庞大,这将有所帮助,但如果它们很轻,则不会有帮助,但如果您有数百万个对象,则会有所帮助。


我认为这符合@StephenGarle所提出的主要评论。很可能有效。 - jdi
我也相信这种方法可以实现干净、理智的实现,而不需要使用任何类黑魔法。(我有点反感使用黑魔法 :-)) - jsbueno
这里有一个更新此示例的建议:http://pastebin.com/dqCTh9LA。展示了`__getattr__`的使用和更灵活的`__setattr__`。 - jdi
请查看 https://github.com/diffoperator/pycow 了解如何对列表、集合和字典进行操作。 - malbarbo

3

不可能。

id(c1) == id(c2)

说 c1 和 c2 是指向完全相同的对象的引用。所以正好等于说。
Python无法区分这两个(除非允许对先前调用帧进行内省,这会导致肮脏的黑客攻击)。
由于这两个赋值是相同的,因此Python无法知道应该导致名称 引用不同的对象。
为了给您一个关于肮脏黑客攻击的想法,
import traceback
import re
import sys
import weakref

class Card(object):
    _CardPool = weakref.WeakValueDictionary()
    def __new__(cls, value, suit):
        obj = Card._CardPool.get(value + suit, None)
        if not obj:
            obj = object.__new__(cls)
            Card._CardPool[value + suit] = obj
            obj._value, obj._suit = value, suit
        return obj
    @property
    def suit(self):
        return self._suit
    @suit.setter
    def suit(self, suit):
        filename,line_number,function_name,text=traceback.extract_stack()[-2]
        name = text[:text.find('.suit')]
        setattr(sys.modules['__main__'], name, Card(self._value, suit))

c1 = Card('10', 'd')
c2 = Card('10', 'd')
assert id(c1) == id(c2)

c2.suit = 's'
print(c1.suit)
# 'd'

assert id(c1) != id(c2)

此 traceback 仅适用于使用框架的 Python 实现,例如 CPython,而不适用于 Jython 或 IronPython。
另一个问题是:
name = text[:text.find('.suit')]

非常脆弱,如果分配任务的方式如下所示,可能会出现问题:
if True: c2.suit = 's'

或者
c2.suit = (
    's')

或者

setattr(c2, 'suit', 's')

另一个问题是它假设名称c2是全局的。它同样可以是一个局部变量(比如在函数内),或者是一个属性(obj.c2.suit = 's')。

我不知道如何解决所有这些赋值方式。

在任何这些情况下,这个不好的技巧都会失败。

结论:不要使用它。 :)


感谢您详尽的答复。要是我自己动手拼凑起来,可能得花费一些时间,并且不会知道所有它失败的情况。看起来@BrendenBrown提出的解决方案最接近我所期望的。 - DMack

0

在你目前的形式下,这是不可能的。一个名称(例如你的示例中的c1c2一个引用,你不能仅仅通过使用__setattr__来更改引用,更不用说所有指向同一对象的其他引用了。

唯一可能的方式是像这样:

c1 = c1.changesuit("s")

c1.changesuit 返回一个(新创建的)对象的引用。但是,只有在每个对象仅由一个名称引用时才有效。或者,您可能能够使用locals()等方法进行一些魔法操作,但请不要这样做。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接