Как мога да запомня инстанция на клас в Python?

Добре, ето сценария от реалния свят: пиша приложение и имам клас, който представлява определен тип файлове (в моя случай това са снимки, но този детайл е без значение за проблема). Всеки екземпляр на класа Photograph трябва да бъде уникален за името на файла на снимката.

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

За мен това изглежда като добра ситуация за използване на мемоизация и има много примери за това, но в този случай аз не просто мемоизирам обикновена функция, трябва да мемоизирам __init__(). Това създава проблем, тъй като докато __init__() бъде извикан, вече е твърде късно, тъй като вече има създаден нов екземпляр.

В моето проучване открих метода __new__() на Python и всъщност успях да напиша работещ тривиален пример, но той се разпадна, когато се опитах да го използвам върху моите обекти от реалния свят и не съм сигурен защо (единственото нещо, което за което мога да се сетя е, че моите обекти от реалния свят бяха подкласове на други обекти, които не мога да контролирам наистина, и така имаше някои несъвместимости с този подход). Ето какво имах:

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.

Някой знае ли къде сбърках тук? Има ли по-добър начин да направите това?

Друг начин да мислим за това е, че е подобно на сингълтън, с изключение на това, че не е глобално сингълтон, а само сингълтон на име на файл.

Ето моя код от реалния свят, използващ статичния метод get, ако искате да го видите всички заедно.


person robru    schedule 04.06.2012    source източник
comment
Редактирах въпроса, за да премахна нещата, които казахте.   -  person robru    schedule 06.06.2012


Отговори (3)


Нека да видим две точки във вашия въпрос.

Използване на memoize

Можете да използвате мемоизация, но трябва да украсите класа, а не метода __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

Както и да е, бих предпочел да създам функция за генериране на обектите и запаметяването им. Изглежда ми по-чисто, но може да е някакво неуместно раздразнение:

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__ метод.

Аз, например, бих предпочел да запаметя функция, която създава обектите, или дори да напиша специфична функция, която да се погрижи никога да не пресъздава обект към същия параметър. Разбира се обаче, това е предимно мое мнение, а не правило.

person brandizzi    schedule 04.06.2012
comment
Благодаря. Не разбрах, че можете да поставите декоратора директно върху класа, вместо върху методите. Това беше ключовата информация, която ми липсваше. Вашият memoize декоратор не е точно това, от което се нуждая, защото низовете не са единични като числата (и следователно ids не са уникални от един идентичен низ към друг), но за моите опростени нужди успях просто да използвате първия аргумент директно като ключ. - person robru; 06.06.2012
comment
@Robru със сигурност моето memoize е просто бърз код, който използвам в примери, не му обръщайте много внимание :) - person brandizzi; 06.06.2012
comment
Разбира се, след един час усъвършенстване на вашия memoize декоратор, за да работи с моята конкретна конфигурация от класове, ми хрумва, че това решение всъщност няма да работи, защото имам редица методи и функции, които итерират върху ClassName.instances dict, за да извършва операции върху всички заредени екземпляри и тази конкретна техника за запаметяване смесва всички различни екземпляри от различни класове в един единствен dict. Изглежда, че все пак ще трябва да отида с __new__. - person robru; 06.06.2012
comment
След още няколко часа човъркане се отказах от __new__ и се върнах при декоратора. Накарах го да работи точно както исках, включително функционални статични методи! (декораторите прекъсват статичните методи по подразбиране, защото оригиналният клас се крие зад обекта на декоратора). Решение тук: github.com/robru/gottengeography/blob/ - person robru; 06.06.2012
comment
имайте предвид, че когато входен параметър е низ или уникод, не е гарантирано, че id('string') е уникален. Вместо това трябва да използвате неговия хеш. - person scooterman; 01.07.2013
comment
@Robru Не е гарантирано, че всички еднакви int имат еднакви идентификатори. Не си спомням подробностите, но ако проверите 10**10 срещу 100**5, ще откриете, че са равни, но не споделят един и същ идентификатор. iirc има някакъв максимален брой, над който python спира да извлича съществуващия обект за ints. - person CrazyCasta; 18.09.2015
comment
@brandizzi Не си параноичен да не украсяваш класове. От една страна, освен ако не пропускам нещо, не можете да разширите украсения клас. - person CrazyCasta; 18.09.2015
comment
@Robru P.S. Според PyInt_FromLong docs.python.org/2/c-api /int.html#c.PyInt_FromLong единствените стойности, които запазват свойството id(a)==id(b) ако a==b са от -5 до 256. Тествах 257 и го прави наистина имат различни идентификатори, ако го инстанциирате няколко пъти. - person CrazyCasta; 18.09.2015
comment
Друго ограничение на подхода decorate-the-class е, че вашият клас обект вече не е всъщност класът, това е функция за обвивка, върната от вашия декоратор. За нормална употреба (извикване за инстанциране на обект) това е добре, но ако се опитате да го използвате с напр. isinstance или issubclass или какъвто и да е вид интроспекция, ще има неочаквани резултати. - person Carl Meyer; 20.01.2017
comment
Когато казвате Както и да е, бих предпочел да създам функция за генериране на обектите и запаметяването им., не е ли тази функция get_test_from_value точно фабрика? Значи ние харесваме фабриките? - person Lars Ericson; 08.06.2018

Решението, което в крайна сметка използвах, е следното:

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.instances или Camera.generate_id() (статичен метод), всъщност не можах да осъществя достъп до тях, защото Photograph всъщност не се отнася до оригиналния клас Photograph, а до memoized функцията (от примера на Brandizzi).

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

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

person robru    schedule 06.06.2012
comment
Това беше много полезно за мен. Само ще добавя, че моят случай на използване включваше и класови методи и следователно изискваше добавяне на тези редове след проверката на статичния метод: if type(val) is classmethod: self.__dict__[attr] = functools.partial(val.__func__, cls) - person MarcTheSpark; 24.10.2018

Параметрите на __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'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.

*/
person John La Rooy    schedule 04.06.2012
comment
Това, което казвате, има смисъл, но как работи моят тривиален пример, без изобщо да дефинирам __init__? Не трябва ли също да ми дава грешки за неправилен брой предадени аргументи? - person robru; 06.06.2012
comment
@Robru, актуализирах отговора си с обяснението, дадено в typeobject.c - person John La Rooy; 06.06.2012