Как я могу 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, получает, если вы хотите увидеть все это вместе.
Ответы
Ответ 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.
*/