如何在Python中记忆类实例化?

时间:2012-06-04 09:32:08

标签: python caching singleton unique memoization

好的,这是现实世界的场景:我正在编写一个应用程序,我有一个代表某种类型文件的类(在我的例子中,这是照片,但细节与问题无关)。 Photos类的每个实例都应该是照片文件名的唯一实例。

问题是,当用户告诉我的应用程序加载文件时,我需要能够识别文件何时已加载,并使用现有实例作为该文件名,而不是在同一文件名上创建重复实例。

对我而言,使用memoization似乎是一个很好的情况,并且有很多例子,但在这种情况下,我不只是记住一个普通的函数,我需要记住__init__() 。这会产生一个问题,因为当__init__()被调用时,它已经太晚了,因为已经创建了一个新实例。

在我的研究中,我发现了Python的__new__()方法,我实际上能够编写一个简单的工作示例,但当我尝试在我的真实世界对象上使用它时它就崩溃了,我不是确定原因(我唯一能想到的是我的真实世界对象是我无法控制的其他对象的子类,因此这种方法存在一些不兼容性)。这就是我所拥有的:

class Flub(object):
    instances = {}

    def __new__(cls, flubid):
        try:
            self = Flub.instances[flubid]
        except KeyError:
            self = Flub.instances[flubid] = super(Flub, cls).__new__(cls)
            print 'making a new one!'
            self.flubid = flubid
        print id(self)
        return self

    @staticmethod
    def destroy_all():
        for flub in Flub.instances.values():
            print 'killing', flub


a = Flub('foo')
b = Flub('foo')
c = Flub('bar')

print a
print b
print c
print a is b, b is c

Flub.destroy_all()

哪个输出:

making a new one!
139958663753808
139958663753808
making a new one!
139958663753872
<__main__.Flub object at 0x7f4aaa6fb050>
<__main__.Flub object at 0x7f4aaa6fb050>
<__main__.Flub object at 0x7f4aaa6fb090>
True False
killing <__main__.Flub object at 0x7f4aaa6fb050>
killing <__main__.Flub object at 0x7f4aaa6fb090>

这很完美!给出了两个唯一ID,只有两个实例,而Flub.instances显然只有两个列出。

但是当我尝试用我正在使用的对象采用这种方法时,我得到了各种无意义的错误,关于__init__()如何只采用0个参数,而不是2.所以我会改变一些事情,然后它会告诉我__init__()需要一个论点。完全奇怪。

经过一段时间的战斗,我基本上只是放弃并将所有__new__()黑魔法移动到一个名为get的静态方法中,以便我可以调用Photograph.get(filename)并且它会如果文件名不在Photograph(filename)中,则只调用Photograph.instances

有人知道我在哪里出错吗?有没有更好的方法来做到这一点?

另一种思考方式是它与单身人士相似,除了它不是全局单身人士,只是每个文件名单身。

Here's my real-world code using the staticmethod get如果你想一起看。

3 个答案:

答案 0 :(得分:15)

让我们看一下你的问题。

使用memoize

您可以使用memoization,但是您应该装饰,而不是__init__方法。假设我们有这个备忘录:

def get_id_tuple(f, args, kwargs, mark=object()):
    """ 
    Some quick'n'dirty way to generate a unique key for an specific call.
    """
    l = [id(f)]
    for arg in args:
        l.append(id(arg))
    l.append(id(mark))
    for k, v in kwargs:
        l.append(k)
        l.append(id(v))
    return tuple(l)

_memoized = {}
def memoize(f):
    """ 
    Some basic memoizer
    """
    def memoized(*args, **kwargs):
        key = get_id_tuple(f, args, kwargs)
        if key not in _memoized:
            _memoized[key] = f(*args, **kwargs)
        return _memoized[key]
    return memoized

现在你只需要装饰这个类:

@memoize
class Test(object):
    def __init__(self, somevalue):
        self.somevalue = somevalue

让我们看一下测试?

tests = [Test(1), Test(2), Test(3), Test(2), Test(4)]
for test in tests:
    print test.somevalue, id(test)

输出如下。请注意,相同的参数会返回返回对象的相同ID:

1 3072319660
2 3072319692
3 3072319724
2 3072319692
4 3072319756

无论如何,我更愿意创建一个函数来生成对象并记住它。对我来说似乎更干净,但它可能是一些无关紧要的宠儿:

class Test(object):
    def __init__(self, somevalue):
        self.somevalue = somevalue

@memoize
def get_test_from_value(somevalue):
    return Test(somevalue)

使用__new__

或者,当然,您可以覆盖__new__。几天前我发布了an answer about the ins, outs and best practices of overriding __new__,这可能会有所帮助。基本上,它表示始终将*args, **kwargs传递给您的__new__方法。

对于一个人来说,我更愿意记住一个创建对象的函数,或者甚至编写一个特定的函数,它将负责永远不会将对象重新创建到同一个参数。然而,当然,这主要是我的意见,而非一般规则。

答案 1 :(得分:4)

我最终使用的解决方案是:

class memoize(object):
    def __init__(self, cls):
        self.cls = cls
        self.__dict__.update(cls.__dict__)

        # This bit allows staticmethods to work as you would expect.
        for attr, val in cls.__dict__.items():
            if type(val) is staticmethod:
                self.__dict__[attr] = val.__func__

    def __call__(self, *args):
        key = '//'.join(map(str, args))
        if key not in self.cls.instances:
            self.cls.instances[key] = self.cls(*args)
        return self.cls.instances[key]

然后用这个装饰,而不是__init__。尽管brandizzi为我提供了关键信息,但他的示例装饰器并没有按照预期运行。

我发现这个概念非常微妙,但基本上当你在Python中使用装饰器时,你需要了解被装饰的东西(无论是方法还是类)实际上被装饰者本身。例如,当我尝试访问Photograph.instancesCamera.generate_id()(静态方法)时,我实际上无法访问它们,因为Photograph实际上并未引用原始的“照片类”,它指的是memoized 函数(来自brandizzi的例子)。

为了解决这个问题,我必须创建一个装饰器类,它实际上从装饰类中获取了所有属性和静态方法,并将它们暴露为它们自己的。几乎像一个子类,除了装饰器类提前不知道它将要装饰什么类,所以它必须在事后复制属性。

最终结果是memoize类的任何实例都变成了它所装饰的实际类的几乎透明的包装,除了尝试实例化它(但实际上是它)将为您提供当它们可用时缓存副本。

答案 2 :(得分:2)

__new__的参数也会传递给__init__,所以:

def __init__(self, flubid):
    ...

您需要在那里接受flubid参数,即使您未在__init__

中使用它

以下是typeobject.c in Python2.7.3

的相关评论
/* You may wonder why object.__new__() only complains about arguments
   when object.__init__() is not overridden, and vice versa.

   Consider the use cases:

   1. When neither is overridden, we want to hear complaints about
      excess (i.e., any) arguments, since their presence could
      indicate there's a bug.

   2. When defining an Immutable type, we are likely to override only
      __new__(), since __init__() is called too late to initialize an
      Immutable object.  Since __new__() defines the signature for the
      type, it would be a pain to have to override __init__() just to
      stop it from complaining about excess arguments.

   3. When defining a Mutable type, we are likely to override only
      __init__().  So here the converse reasoning applies: we don't
      want to have to override __new__() just to stop it from
      complaining.

   4. When __init__() is overridden, and the subclass __init__() calls
      object.__init__(), the latter should complain about excess
      arguments; ditto for __new__().

   Use cases 2 and 3 make it unattractive to unconditionally check for
   excess arguments.  The best solution that addresses all four use
   cases is as follows: __init__() complains about excess arguments
   unless __new__() is overridden and __init__() is not overridden
   (IOW, if __init__() is overridden or __new__() is not overridden);
   symmetrically, __new__() complains about excess arguments unless
   __init__() is overridden and __new__() is not overridden
   (IOW, if __new__() is overridden or __init__() is not overridden).

   However, for backwards compatibility, this breaks too much code.
   Therefore, in 2.6, we'll *warn* about excess arguments when both
   methods are overridden; for all other cases we'll use the above
   rules.

*/