【问题标题】:Unique representations of Python objectsPython 对象的唯一表示
【发布时间】:2013-08-28 21:45:27
【问题描述】:

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

现在考虑说明

x = C(0)
y = C(0)

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

是否可以强制 x 和 y 共享内存中的同一个位置?

如果某个 Python 装饰器能完成这项工作,我会非常高兴。

[注意]我正在寻找一种方法来记忆构造函数(有关函数的记忆,请参阅http://en.wikipedia.org/wiki/Memoization)。

[添加] Sage 开源数学软件通过UniqueRepresentation 类(见here)为这个问题提供了很好的解决方案。任何类都应该从这个类继承以获得预期的行为。不过,我想知道这个问题是否有纯 Python 解决方案。

【问题讨论】:

  • 呃...为什么不x = y = C(0)?或x = C(0); y=x?
  • 但是为什么 x = y = C(0)?当两个对象在数学上相等时,我希望它们共享相同的内存位置。假设 x = C(0) 被定义为函数 f 的局部变量,y = C(0) 被定义为另一个函数 g 的局部变量。
  • 您是否在考虑按需复制之类的东西?比如当你fork() 时会发生什么?此外,x==y 意义上的平等测试是 __eq__ 的用途
  • 我正在寻找一种方法来记忆构造函数。
  • 请解释memoization of constructors

标签: python object caching constructor decorator


【解决方案1】:

您可能想使用lru_cache。如果你的类定义是

@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 成为一个函数,并且在实际实例化该类之前,任何类功能都不可用。如果需要,也可以直接缓存方法__new__,它负责对象的创建。 __new__ 是一个方法,它采用与 __init__ 相同的所有参数,并且在我们创建类实例时在 __init__ 之前调用。

由于缓存__new__ 的输出很简单,我们可以让事情变得更有趣。让我们创建一个新的装饰器,它的工作方式与lru_cache 类似,但它可以与类一起使用来缓存__new__ 的输出:

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

我们为__new__ 提供所有可能的参数和关键字参数,以便它也可以与其他类一起使用。现在我们可以像这样缓存C2 类的实例:

@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

这种行为是意料之中的,因为无论如何都会调用__init__,尽管对象的内存位置保持不变。根据您的用例,您可能还希望缓存 __init__ 的输出。

【讨论】:

  • 这是一个不错的解决方案,但问题是装饰器@lru_cache 将符号C 变成了函数标识符。是否有类似的解决方案可以保留 C 是一个类的事实?
  • 你是对的。我刚刚编辑了我的答案以显示另一种方式,它将名称 C 保留为一个类。
  • 谢谢,这看起来很棒!尽管如此,拥有一个类装饰器来获得相同的行为会非常有用。
  • 您的解决方案非常有趣,但我认为存在一些问题。您的类装饰器从输入类创建一个新类。这个新类失去了第一个类的一些属性,比如它的文档字符串。我认为一个出色的解决方案不会创建并返回一个新类,而只是将__new__ 的记忆版本添加到输入类。你觉得这可行吗?
  • 重写方法(在这种情况下为__new__)只能通过继承实现。您对类文档字符串的担忧很容易解决——只需将__doc__ 属性定义为父类的属性。如果您还想要一些指向基类的漂亮字符串表示,则需要在定义中指定__str__ 方法。但是您所期望的出色解决方案只是在您的类定义C 中使用lru_cache 装饰器定义__new__(这是我在上次编辑之前建议的)。
【解决方案2】:

您可以直接覆盖 __new__ 来存储每个对象的缓存版本:

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

【讨论】:

    【解决方案3】:

    如果您愿意让一个函数为您创建类实例,这可能会起作用。假设您的班级 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
    

    我正在利用这样一个事实,即如果您使用可变对象作为函数的默认参数,则每次调用该函数时都会使用 same 可变对象。

    编辑

    如果您想让这个通用(假设您的所有类都需要一个数字),您可以执行以下操作:

    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 = getter(C, 0)
    >>> b = getter(C, 0)
    >>> c = getter(A, 0)
    

    【讨论】:

    • 感谢您的回答!但是,这个解决方案在以下意义上不是通用的:您必须为每个类 A 定义一个函数 A_getter。而且,我认为将这个函数定义为静态方法或C的类方法更准确。这也很好,但我认为遗憾的是(1)必须不通过构造函数来构造对象,( 2) 必须为任何类定义相同的静态(或类)方法。
    • @SamueleGiraudo 我已经让它更通用了,但它仍然不能满足你的所有标准。
    • 这是一个很好的解决方案,即使它确实不满足我的所有标准(我无法投票,因为我没有足够的声誉,抱歉)。也许,可以通过将参数num 替换为*args 来稍微改进它,以涵盖构造函数需要任何参数的情况。
    • @SamueleGiraudo 我想到了这一点,但在 python 2.x 中,这需要_class_archive 之前 *args,然后你必须提供一个参数调用函数时为_class_archive,完全违背了目的。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-03-09
    • 2021-09-21
    • 2022-01-06
    • 1970-01-01
    • 1970-01-01
    • 2023-03-16
    相关资源
    最近更新 更多