【问题标题】:How can I memoize a class instantiation in Python?如何在 Python 中记忆类实例化?
【发布时间】:2012-06-08 09:33:27
【问题描述】:

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

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

对我来说,这似乎是一个使用记忆的好情况,并且有很多这样的例子,但在这种情况下,我不只是记忆一个普通的函数,我需要记忆__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) if文件名不在Photograph.instances 中。

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

另一种思考方式是,它类似于单例,只是它不是全局单例,只是每个文件名的单例。

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

【问题讨论】:

  • 我已编辑问题以删除您所说的内容。

标签: python caching singleton unique memoization


【解决方案1】:

让我们看看关于您的问题的两点。

使用备忘录

你可以使用 memoization,但你应该装饰 class,而不是 __init__ 方法。假设我们有这个 memoizator:

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__ 方法。

一方面,我更喜欢记忆一个创建对象的函数,或者甚至编写一个特定的函数来处理永远不会为相同参数重新创建对象的函数。当然,不过,这主要是我的意见,而不是规则。

【讨论】:

  • 谢谢。我没有意识到您可以将装饰器直接放在类而不是方法上。这是我遗漏的关键信息。你的 memoize 装饰器不是我需要的,因为字符串不像数字那样是单例(因此ids 在一个相同的字符串之间不是唯一的),而是为了我的简化需求我能够直接使用第一个参数作为键。
  • @Robru 当然我的 memoize 只是我在示例中使用的一些快速代码,不要太在意它:)
  • 又折腾了几个小时,我放弃了__new__,回到了装饰器。我让它完全按我的意愿工作,包括功能性静态方法! (装饰器默认会破坏静态方法,因为原始类隐藏在装饰器对象后面)。解决方案:github.com/robru/gottengeography/blob/…
  • 请注意,当输入参数是字符串或 unicode 时,不保证 id('string') 是唯一的。您应该改用它的哈希值。
  • @Robru P.S.根据 PyInt_FromLong docs.python.org/2/c-api/int.html#c.PyInt_FromLong 如果 a==b 保留属性 id(a)==id(b) 的唯一值是从 -5 到 256。我测试了 257,如果你确实有不同的 id多次实例化它。
【解决方案2】:

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

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]

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

我发现这个概念非常微妙,但基本上当您在 Python 中使用装饰器时,您需要了解被装饰的东西(无论是方法还是类)实际上是 替换为 装饰器本身。因此,例如,当我尝试访问 Photograph.instancesCamera.generate_id()(静态方法)时,我实际上无法访问它们,因为 Photograph 实际上并不是指原始照片类,而是指 @987654326 @ function(来自brandizzi 的示例)。

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

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

【讨论】:

  • 这对我很有帮助。我将补充一点,我的用例也涉及类方法,因此需要在静态方法检查之后添加这些行:if type(val) is classmethod: self.__dict__[attr] = functools.partial(val.__func__, cls)
【解决方案3】:

__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.

*/

【讨论】:

  • 你说的很有道理,但是我这个简单的例子在没有定义 __init__ 的情况下是如何工作的?它不应该也给我关于传递的参数数量不正确的错误吗?
  • @Robru,我用typeobject.c中给出的解释更新了我的答案
猜你喜欢
  • 2019-01-20
  • 1970-01-01
  • 1970-01-01
  • 2015-03-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-05-24
  • 1970-01-01
相关资源
最近更新 更多