Подтвердить что ты не робот

Как я могу memoize экземпляр класса в Python?

Хорошо, вот сценарий реального мира: я пишу приложение, и у меня есть класс, который представляет определенный тип файлов (в моем случае это фотографии, но эта деталь не имеет отношения к проблеме). Каждый экземпляр класса Photograph должен быть уникальным для имени файла фотографии.

Проблема заключается в том, что когда пользователь сообщает моему приложению о загрузке файла, мне нужно определить, когда файлы уже загружены, и использовать существующий экземпляр для этого имени файла, а не создавать дубликаты экземпляров в одном и том же имени файла.

Мне кажется, что это хорошая ситуация для использования memoization, и там много примеров этого, но в этом случае я не просто memoizing обычной функции, мне нужно memoising __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>

Это прекрасно! Только два экземпляра были сделаны для двух уникальных идентификаторов, и у Flub.instances явно только два указанных.

Но когда я попытался использовать этот подход с объектами, которые я использовал, у меня были всевозможные бессмысленные ошибки о том, как __init__() принимало только 0 аргументов, а не 2. Поэтому я бы изменил некоторые вещи, и тогда это скажите, что __init__() нужен аргумент. Совершенно странно.

После некоторого времени сражения с ним я просто сдался и переместил всю черную магию __new__() в статический метод под названием get, чтобы я мог вызвать Photograph.get(filename), и он будет вызывать только Photograph(filename), если имя файла еще не было в Photograph.instances.

Кто-нибудь знает, где я здесь ошибся? Есть ли лучший способ сделать это?

Еще один способ подумать о том, что он похож на синглтон, за исключением того, что он не глобально singleton, просто singleton-per-filename.

Здесь мой реальный код, использующий staticmethod, получает, если вы хотите увидеть все это вместе.

4b9b3361

Ответ 1

Давайте посмотрим два вопроса о вашем вопросе.

Использование 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)

Выход ниже. Обратите внимание, что те же параметры дают один и тот же идентификатор возвращаемого объекта:

1 3072319660
2 3072319692
3 3072319724
2 3072319692
4 3072319756

Во всяком случае, я бы предпочел создать функцию для генерации объектов и memoize. Мне кажется, что это чище, но это может быть какое-то неуместное домашнее животное:

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

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

Использование __new__:

Или, конечно, вы можете переопределить __new__. Несколько дней назад я опубликовал ответ об ошибках, рекомендациях и методах переопределения __new__, которые могут быть полезны. В основном, он говорит, чтобы всегда передавать *args, **kwargs вашему методу __new__.

Я, например, предпочел бы memoize функцию, которая создает объекты, или даже написать определенную функцию, которая позаботится о том, чтобы никогда не воссоздавать объект с тем же параметром. Конечно, однако, это в основном мое мнение, а не правило.

Ответ 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]

И тогда вы украшаете класс этим, а не __init__. Хотя брендиззи предоставил мне эту ключевую информацию, его декоратор-декодер не работал по желанию.

Я нашел эту концепцию довольно тонкой, но в основном, когда вы используете декораторы на Python, вам нужно понять, что вещь, которая украшена (будь то метод или класс), фактически заменяется самим декоратором. Например, когда я попытаюсь получить доступ к Photograph.instances или Camera.generate_id() (статический метод), я не смог бы фактически получить к ним доступ, потому что Photograph на самом деле не ссылается на исходный класс Photograph, он ссылается на memoized функция (от примера brandizzi).

Чтобы обойти это, мне пришлось создать класс декоратора, который фактически взял все атрибуты и статические методы из украшенного класса и разоблачил их как свои собственные. Почти как подкласс, за исключением того, что класс декоратора заранее не знает, какие классы он будет украшать, поэтому он должен скопировать атрибуты после факта.

Конечным результатом является то, что любой экземпляр класса memoize становится почти прозрачной оболочкой вокруг фактического класса, который он украсил, за исключением того, что пытается создать его экземпляр (но действительно вызывает его) предоставит вам кешированные копии когда они доступны.

Ответ 3

Параметры __new__ также передаются в __init__, поэтому:

def __init__(self, flubid):
    ...

Вам нужно принять аргумент flubid, даже если вы не используете его в __init__

Вот соответствующий комментарий, взятый из typeobject.c в 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 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.

*/