Python对象的唯一表示形式

Python对象的唯一表示形式,python,object,caching,constructor,decorator,Python,Object,Caching,Constructor,Decorator,假设C是一个Python类,并假设C的构造函数以整数作为参数 现在考虑一下说明 x = C(0) y = C(0) Python的默认行为意味着x和y在内存中占据两个不同的位置 可以强制x和y在内存中共享同一个位置吗 如果某个Python装饰师能胜任这项工作,我会非常高兴 [注意]我正在寻找一种记忆构造函数的方法(有关函数的记忆,请参阅) [Add]Sage开源数学软件通过类UniqueRepresentation(请参阅)为这个问题提供了一个非常好的解决方案。任何类都应从该类继承,以具有预期

假设C是一个Python类,并假设C的构造函数以整数作为参数

现在考虑一下说明

x = C(0)
y = C(0)
Python的默认行为意味着x和y在内存中占据两个不同的位置

可以强制x和y在内存中共享同一个位置吗

如果某个Python装饰师能胜任这项工作,我会非常高兴

[注意]我正在寻找一种记忆构造函数的方法(有关函数的记忆,请参阅)


[Add]Sage开源数学软件通过类
UniqueRepresentation
(请参阅)为这个问题提供了一个非常好的解决方案。任何类都应从该类继承,以具有预期的行为。然而,我想知道是否有一个纯Python的解决方案来解决这个问题。

如果您愿意让一个函数为您创建类实例,这可能会奏效。假设您的类
C
接受整数:

def C_getter(num, _class_archive={}):
    """\
    Returns an instance of the `C` class,
    making sure that if an object already exists with that
    integer number a new object is not created.

    The _class_archive is used to keep a record of all the instances
    in memory local to this function.  Don't actually supply an
    argument to _class_archive when you call this function.
    """

    if num not in _class_archive:
        _class_archive[num] = C(num)
    return _class_archive[num]
像这样使用它:

>>> a = C_getter(0)
>>> b = C_getter(0)
>>> a is b
True
>>> c = C(0)
>>> a is c
False
>>> a = getter(C, 0)
>>> b = getter(C, 0)
>>> c = getter(A, 0)
@lru_cache_class(maxsize=32)
class C2(object):
    def __init__(self, num):
        self.num = num
我利用了这样一个事实:如果使用可变对象作为函数的默认参数,那么每次调用函数时都会使用相同的可变对象

编辑

如果要使其成为泛型(假设所有类都需要一个数字),可以执行以下操作:

def getter(your_class, num, _class_archive={}):
    if (your_class, num) not in _class_archive:
        _class_archive[(your_class, num)] = your_class(num)
    return _class_archive[(your_class, num)]
您可以这样使用它:

>>> a = C_getter(0)
>>> b = C_getter(0)
>>> a is b
True
>>> c = C(0)
>>> a is c
False
>>> a = getter(C, 0)
>>> b = getter(C, 0)
>>> c = getter(A, 0)
@lru_cache_class(maxsize=32)
class C2(object):
    def __init__(self, num):
        self.num = num
您可能想使用。如果您的类定义是

@lru_cache(maxsize=32)
class C(object):
    def __init__(self, num):
        self.num = num
那么它的行为就像

>>> a = C(1)
>>> a.num = 2
>>> b = C(1)
>>> b.num
2
>>> a is b
True
但是,这使得名称
C
成为一个函数,在类实际实例化之前,任何类功能都不可用。如果需要,还可以直接缓存负责创建对象的方法
\uuuu new\uuuu
\uuuuuuuuuuuuuu
是一种方法,它采用与
\uuuuuu init\uuuuuuu
相同的所有参数,当我们创建类实例时,它在
\uuuuuu init\uuuuuuuuuuuuu>之前被调用

由于缓存
\uuuuu new\uuuuuu
的输出非常简单,我们可以让事情变得更加有趣。让我们创建一个新的decorator,它的工作原理与
lru\u cache
类似,但它可以与类一起用于缓存
\uuuu new\uuu
的输出:

def lru_cache_class(maxsize):
    def wrap(klass):
        @lru_cache(maxsize=maxsize)
        def new(cls, *args, **kwargs):
            self = object.__new__(cls)
            return self
        klass.__new__ = new
        return klass
    return wrap
我们给出了
\uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu。现在我们可以像这样缓存class
C2
的实例:

>>> a = C_getter(0)
>>> b = C_getter(0)
>>> a is b
True
>>> c = C(0)
>>> a is c
False
>>> a = getter(C, 0)
>>> b = getter(C, 0)
>>> c = getter(A, 0)
@lru_cache_class(maxsize=32)
class C2(object):
    def __init__(self, num):
        self.num = num
我们可以看到对象被缓存:

>>> c = C2(2)
>>> c is C2(2)
True
然而,与第一种方法相比,这种方法还有一个细微的区别。例如:

>>> d = C2(3)
>>> d.num = 4
>>> d.num
4
>>> e = C2(3)
>>> d.num == e.num
>>> d.num
3

这种行为是预期的,因为尽管对象的内存位置保持不变,但仍会调用
\uuuu init\uuuu
。根据您的使用情况,您可能还需要缓存
\uuuuu init\uuuu
的输出。

您只需覆盖
\uuuu new\uuuu
即可存储每个对象的缓存版本:

class C(object):
    _cache = {}

    def __new__(cls, x):
        if x not in C._cache:
            C._cache[x] = object.__new__(cls, x)
        return C._cache[x]

    def __init__(self, x):
        self.x = x
演示:

>>> a = C(1)
>>> b = C(1)
>>> a is b
True
>>> id(a) == id(b)
True
显然,如果您以后更改
x
而不是创建新类,它将不会成为与先前定义的值为
x
的对象相同的对象:

>>> a = C(1)
>>> b = C(2)
>>> a.x = 2
>>> a is b
False

嗯。。。为什么不
x=y=C(0)
?或
x=C(0);y=x
?但为什么x=y=C(0)?我想,当两个物体在数学上相等时,它们共享同一个记忆位置。假设x=C(0)被定义为一个函数f的局部变量,y=C(0)被定义为另一个函数g的局部变量。您是否考虑按需复制;比如当您使用
fork()
时会发生什么?另外,测试
x==y
意义上的相等性是
\uuuu eq\uuuu
的目的,我正在寻找一种方法来记忆构造函数。请解释
记忆构造函数
谢谢你的回答!但是,这个解决方案在以下意义上不是通用的:您必须为每个类定义一个函数A_getter。此外,我认为将此函数定义为C的静态方法或类方法更为准确。这也很有效,但我认为遗憾的是,(1)必须通过构造函数构造对象,(2)必须为任何类定义相同的静态(或类)方法。@SamueleGiraudo我使其更为通用,但它仍然不能满足你所有的标准。这是一个很好的解决方案,即使它确实不能满足我所有的标准(对不起,我不能投票,因为我没有足够的声誉)。也许,可以通过将参数
num
替换为
*args
来稍微改进它,以涵盖构造函数需要任何参数的情况。@SamueleGiraudo我想到了这一点,但在python 2.x中,这要求
\u class\u archive
*args
之前,然后在调用函数时,必须为
\u class\u archive
提供一个参数,这完全违背了函数的目的。这是一个很好的解决方案,但问题是decorator
@lru\u cache
将符号
C
转换为函数标识符。有没有类似的解决方案可以保留
C
是类这一事实?你是对的。我刚刚编辑了我的答案以显示另一种方式,它将名称
C
保留为一个类。谢谢,这看起来非常棒!尽管如此,拥有一个类装饰器来获得相同的行为还是非常有用的。类装饰器从输入类创建一个新类。这个新类丢失了第一个类的一些属性,比如它的doctstring。我认为一个优秀的解决方案不是创建并返回一个新类,而是向输入类添加一个
\uuuuunew\uuuuuu
的记忆版本。您认为这是可行的吗?重写方法(在本例中是新的)只有通过继承才能实现。您对类docstring的担忧很容易得到解决——只需将
\uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu属性定义为父类的属性即可。如果您还想要一些漂亮的字符串表示,指向