Python copy-on-write行为

时间:2012-09-10 20:58:58

标签: python python-2.7 flyweight-pattern

我正在研究一个问题,我正在实例化一个对象的许多实例。大多数情况下,实例化的对象是相同的。为了减少内存开销,我想让所有相同的对象指向同一个地址。但是,当我修改对象时,我想要创建一个新实例 - 基本上是写时复制行为。在Python中实现这一目标的最佳方法是什么?

Flyweight模式接近尾声。一个例子(来自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

更新:我遇到了Flyweight模式,它似乎几乎适合该法案。但是,我对其他方法持开放态度。

3 个答案:

答案 0 :(得分:6)

您是否需要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公开属性。

你仍然有很多重复的对象,但只有一个副本的“真实”支持对象。因此,如果每个对象都很庞大,这将有所帮助,如果它们是轻量级的,那将无济于事,但是你有数百万个。

答案 1 :(得分:3)

不可能。

id(c1) == id(c2)

表示c1c2是对完全相同对象的引用。所以

c2.suit = 's'c1.suit = 's'完全相同。

Python没有办法区分这两者(除非你允许对先前的调用帧进行内省,这会导致肮脏的黑客攻击。)

由于这两个分配是相同的,因此Python无法知道c2.suit = 's'应该导致名称 c2引用不同的对象。


为了让您了解脏黑客的样子,

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)

这种回溯的使用仅适用于那些使用框架的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')。

我不知道如何解决分配的所有方式。

在任何一种情况下,肮脏的黑客都会失败。

结论:不要使用它。 :)

答案 2 :(得分:0)

目前的形式是不可能的。您的示例中的名称(c1c2 是引用,您不能只使用__setattr__更改引用,更不用说所有其他引用同一个对象。

唯一可行的方法是:

c1 = c1.changesuit("s")

c1.changesuit返回对(新创建的)对象的引用。但这只适用于每个对象仅由一个名称引用的情况。或者你可能能够使用locals()和类似的东西做一些魔法,但请 - 不要。