2012-06-04 10 views
13

OK, oto scenariusz z prawdziwego świata: piszę aplikację i mam klasę reprezentującą pewien typ plików (w moim przypadku to zdjęcia, ale ten szczegół jest nieistotny dla problemu). Każde wystąpienie klasy Photograph powinno być unikalne dla nazwy pliku zdjęcia.Jak mogę zapamiętać wystąpienie klasy w Pythonie?

Problem polega na tym, że kiedy użytkownik mówi mojej aplikacji, aby załadował plik, muszę być w stanie określić, kiedy pliki są już załadowane i użyć istniejącej instancji dla tej nazwy pliku, zamiast tworzyć zduplikowane wystąpienia na tej samej nazwie pliku .

Wydaje mi się, że to dobra sytuacja, aby użyć funkcji zapamiętywania. Jest wiele takich przykładów, ale w tym przypadku nie pamiętam tylko zwykłej funkcji, muszę zapamiętać numer __init__(). Stanowi to problem, ponieważ do czasu, gdy zostanie wywołany __init__(), jest już za późno, ponieważ jest już utworzona nowa instancja.

W moich badań znalazłem __new__() metody Pythona, a ja rzeczywiście w stanie napisać pracę banalny przykład, ale to rozpadło się, gdy próbowałem go używać na moich przedmiotów rzeczywistych, a nie jestem pewien, dlaczego (jedyne, o czym mogę pomyśleć to to, że moje obiekty z prawdziwego świata były podklasami innych obiektów, których tak naprawdę nie mogę kontrolować, a więc istniały pewne niezgodności z tym podejściem). To co miałem:

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() 

Które wyjście to:

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> 

Jest to idealne rozwiązanie! Zostały wykonane tylko dwa wystąpienia dla dwóch unikalnych identyfikatorów, a Flub.instances ma tylko dwa wymienione.

Ale kiedy próbowałem podejść do tego z obiektami, z których korzystałem, otrzymałem różnego rodzaju bezsensowne błędy dotyczące tego, jak __init__() zajął tylko 0 argumentów, nie 2. Więc zmieniłbym niektóre rzeczy, a potem powiedziałabym, mnie, że __init__() potrzebował argumentu. Całkowicie dziwaczne.

Po chwili walki z nim, ja w zasadzie tylko zrezygnował i przeniósł cały __new__() czarnej magii w staticmethod zwanego get tak, że mogę zadzwonić Photograph.get(filename) i to tylko zadzwonić Photograph(filename) jeśli nazwa pliku nie był już w Photograph.instances .

Czy ktoś wie, gdzie się tu znalazłem? Czy jest jakiś lepszy sposób na zrobienie tego?

Innym sposobem myślenia jest to, że jest podobny do singletonu, z tym wyjątkiem, że nie jest to pojedynczy singleton, tylko singleton-per-filename.

Here's my real-world code using the staticmethod get jeśli chcesz zobaczyć to wszystko razem.

+1

Zmieniłem to pytanie, aby usunąć te rzeczy, które powiedziałeś. – robru

Odpowiedz

11

Zobaczmy dwa punkty na temat twojego pytania.

Korzystanie memoize

Można użyć memoization, ale należy ozdobić klasę , a nie metodę __init__. Załóżmy, że mamy tę 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 

Teraz wystarczy udekorować klasę:

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

Zobaczmy test?

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

Dane wyjściowe znajdują się poniżej. Należy zauważyć, że te same parametry uzyskuje się taki sam identyfikator obiektu zwróconej:

1 3072319660 
2 3072319692 
3 3072319724 
2 3072319692 
4 3072319756 

każdym razie to, że raczej utworzyć funkcję generowania obiektów i memoize go. Wydaje mi się czystsze, ale może to być jakiś nieistotny denerwuje:

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

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

Korzystanie __new__:

Albo, oczywiście, można zastąpić __new__. Kilka dni temu opublikowałem an answer about the ins, outs and best practices of overriding __new__, które może być pomocne. Zasadniczo mówi się, aby zawsze przekazywać *args, **kwargs do swojej metody __new__.

Ja na przykład wolę zapamiętać funkcję, która tworzy obiekty, lub nawet napisać określoną funkcję, która zadba o to, aby nigdy nie odtwarzać obiektu na tym samym parametrze. Oczywiście jest to jednak przede wszystkim moja opinia, a nie reguła.

+0

Dzięki. Nie zdawałem sobie sprawy, że można umieścić dekoratora bezpośrednio na zajęciach zamiast na metodach. To była kluczowa informacja, której mi brakowało. Twój dekorator z pamięcią nie jest * całkiem * tym, czego potrzebuję, ponieważ łańcuchy nie są pojedynczymi znakami takimi jak liczby (i dlatego "identyfikatory" nie są unikalne z jednego identycznego ciągu znaków do drugiego), ale dla moich uproszczonych potrzeb byłem w stanie po prostu użyj pierwszego argumentu bezpośrednio jako klucza. – robru

+0

@Robru na pewno moje memoize to tylko szybki kod, którego używam w przykładach, nie zwracaj na to uwagi :) – brandizzi

+0

Oczywiście, po godzinie podpalania twojego dekoratora memoizera do pracy z moją konkretną konfiguracją zajęć, przychodzi mi to do głowy że to rozwiązanie nie zadziała, ponieważ mam wiele metod i funkcji, które iterują nad 'Dyrektywą Class.interncje' w celu wykonania operacji na wszystkich załadowanych instancjach, a ta konkretna technika zapamiętywania miesza wszystkie różne instancje różnych klas w jeden dyktat. Wygląda na to, że będę musiał iść z '__new__' w końcu. – robru

2

Parametry do __new__ również przejdzie do __init__, więc:

def __init__(self, flubid): 
    ... 

Musisz zaakceptować tam flubid argumentu, nawet jeśli nie należy go używać w __init__

Oto odnośny komentarz zaczerpnięty z 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. 

*/ 
+0

To, co mówisz, ma sens, ale w jaki sposób mój trywialny przykład działa bez definiowania '__init__' w ogóle? Czy nie powinienem również podawać mi błędów dotyczących niepoprawnej liczby przekazanych argumentów? – robru

+0

@Robru, zaktualizowałem swoją odpowiedź z wyjaśnieniem podanym w 'typeobject.c' –

+0

Oh, ok. Dzięki. – robru

3

Rozwiązanie, które skończyło się przy użyciu to:

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] 

A potem ozdobić klasęz tym, nie __init__. Chociaż brandizzi dostarczył mi tę kluczową informację, jego przykładowy dekorator nie działał tak, jak chciał.

Znalazłem to pojęcie dość subtelna, ale w zasadzie, gdy używasz dekoratorów w Pythonie, musisz zrozumieć, że to, co dostaje urządzone (czy jest to metoda lub klasa) jest rzeczywiście zastąpione sam dekorator . Na przykład, gdy spróbuję uzyskać dostęp do Photograph.instances lub (metoda statyczna), nie mogę uzyskać do nich dostępu, ponieważ Photograph w rzeczywistości nie odnosi się do oryginalnej klasy Photograph, odnosi się do funkcji memoized (z przykładu brandizzi).

Aby obejść ten problem, musiałem stworzyć klasę dekoratorów, która faktycznie wzięła wszystkie atrybuty i statyczne metody od udekorowanej klasy i ujawniła je jako własne. Prawie jak podklasa, z tą różnicą, że klasa dekoratorów nie wie z wyprzedzeniem, jakie klasy będzie ozdabiać, więc musi kopiować atrybuty po fakcie.

Wynik jest taki, że każda instancja klasy memoize staje się niemal przezroczystą otoczką otaczającej faktycznej klasy, z wyjątkiem tego, że próba jej utworzenia (ale tak naprawdę wywołanie) zapewni kopie przechowywane w pamięci podręcznej, gdy są dostępne.

Powiązane problemy