2012-06-06 11 views
6

Tak więc niedawno zadałem pytanie dotyczące memoizacji i otrzymałem kilka świetnych odpowiedzi, a teraz chcę zabrać go na wyższy poziom. Po dość googlowaniu nie mogłem znaleźć referencyjnej implementacji dekoratora memoize, który byłby w stanie buforować funkcję, która pobierała argumenty słów kluczowych. W rzeczywistości większość z nich po prostu używała *args jako klucza do wyszukiwań w pamięci podręcznej, co oznacza, że ​​byłby również przerywany, gdybyś chciał zapamiętać funkcję, która akceptowała listy lub dyktuje jako argumenty.Czy istnieje pythonic sposób obsługi argumentów słów kluczowych dla dekoratora memoize w Pythonie?

W moim przypadku pierwszy argument do funkcji jest unikalnym identyfikatorem sam w sobie, odpowiedni do użycia jako klucz dyktujący dla wyszukiwań w pamięci podręcznej, jednak chciałem możliwość używania argumentów słów kluczowych i nadal mieć dostęp do tej samej pamięci podręcznej. Mam na myśli to, że oba zwracają ten sam wynik w pamięci podręcznej.

W tym celu potrzebujemy czystego i pytonowego sposobu powiedzenia "sprawdź kwargs dla dowolnego słowa kluczowego, które odpowiada pierwszemu argumentowi". Oto, co wymyśliłem:

class memoize(object): 
    def __init__(self, cls): 
     if type(cls) is FunctionType: 
      # Let's just pretend that the function you gave us is a class. 
      cls.instances = {} 
      cls.__init__ = cls 
     self.cls = cls 
     self.__dict__.update(cls.__dict__) 

    def __call__(self, *args, **kwargs): 
     """Return a cached instance of the appropriate class if it exists.""" 
     # This is some dark magic we're using here, but it's how we discover 
     # that the first argument to Photograph.__init__ is 'filename', but the 
     # first argument to Camera.__init__ is 'camera_id' in a general way. 
     delta = 2 if type(self.cls) is FunctionType else 1 
     first_keyword_arg = [k 
      for k, v in inspect.getcallargs(
       self.cls.__init__, 
       'self', 
       'first argument', 
       *['subsequent args'] * (len(args) + len(kwargs) - delta)).items() 
        if v == 'first argument'][0] 
     key = kwargs.get(first_keyword_arg) or args[0] 
     print key 
     if key not in self.cls.instances: 
      self.cls.instances[key] = self.cls(*args, **kwargs) 
     return self.cls.instances[key] 

Szaloną rzeczą jest to, że to faktycznie działa. Na przykład, jeśli ozdobić tak:

@memoize 
class FooBar: 
    instances = {} 

    def __init__(self, unique_id, irrelevant=None): 
     print id(self) 

Następnie z kodu można wywołać albo FooBar('12345', 20) lub FooBar(irrelevant=20, unique_id='12345') i rzeczywiście uzyskać tę samą instancję foobar. Następnie możesz zdefiniować inną klasę o innej nazwie dla pierwszego argumentu, ponieważ działa ona w sposób ogólny (tzn. Dekorator nie musi znać żadnych szczegółów dotyczących klasy, którą dekoruje, aby to działało).

Problemem jest to, że jest to bezbożny bałagan ;-)

To działa, ponieważ inspect.getcallargs zwraca dict mapowanie zdefiniowane słowa kluczowe do argumentów go zaopatrują, więc dostarczać go niektóre fałszywe argumenty, a następnie sprawdzić dict dla pierwszy argument, który został przyjęty.

Co by było o wiele ładniej, gdyby coś takiego istniało, jest analogiem do inspect.getcallargs, który zwrócił oba rodzaje argumentów zunifikowane jako lista argumentów, zamiast jako dyktat argumentów słów kluczowych. Które pozwoliłyby coś takiego:

def __call__(self, *args, **kwargs): 
    key = inspect.getcallargsaslist(self.cls.__init__, None, *args, **kwargs)[1] 
    if key not in self.cls.instances: 
     self.cls.instances[key] = self.cls(*args, **kwargs) 
    return self.cls.instances[key] 

inny sposób mogę zobaczyć w walce z tym będzie przy użyciu dict dostarczone przez inspect.getcallargs jako klucz wyszukiwanie cache bezpośrednio, ale to wymagałoby powtarzalny sposób, aby identyczne ciągi z identyczne hashe, o czym słyszałem, nie można polegać (chyba po skonstruowaniu klawiszy muszę samemu skonstruować strunę).

Czy ktoś ma jakieś przemyślenia na ten temat? Czy nie należy wywoływać funkcji z argumentami słów kluczowych i buforować wyniki? Lub po prostu bardzo trudne?

Odpowiedz

4

sugeruję coś jak następuje:

import inspect 

class key_memoized(object): 
    def __init__(self, func): 
     self.func = func 
     self.cache = {} 

    def __call__(self, *args, **kwargs): 
     key = self.key(args, kwargs) 
     if key not in self.cache: 
      self.cache[key] = self.func(*args, **kwargs) 
     return self.cache[key] 

    def normalize_args(self, args, kwargs): 
     spec = inspect.getargs(self.func.__code__).args 
     return dict(kwargs.items() + zip(spec, args)) 

    def key(self, args, kwargs): 
     a = self.normalize_args(args, kwargs) 
     return tuple(sorted(a.items())) 

Przykład:

@key_memoized 
def foo(bar, baz, spam): 
    print 'calling foo: bar=%r baz=%r spam=%r' % (bar, baz, spam) 
    return bar + baz + spam 

print foo(1, 2, 3) 
print foo(1, 2, spam=3)   #memoized 
print foo(spam=3, baz=2, bar=1) #memoized 

nocie można również rozszerzyć key_memoized i zastąpić jego metodę key(), aby zapewnić bardziej szczegółowe strategie zapamiętywania , np. zignorować niektóre argumenty:

class memoize_by_bar(key_memoized): 
    def key(self, args, kwargs): 
     return self.normalize_args(args, kwargs)['bar'] 

@memoize_by_bar 
def foo(bar, baz, spam): 
    print 'calling foo: bar=%r baz=%r spam=%r' % (bar, baz, spam) 
    return bar 

print foo('x', 'ignore1', 'ignore2') 
print foo('x', 'ignore3', 'ignore4') 
+0

Podoba mi się wygląd tego, ale niepokoi mnie część, w której 'key' zwraca' tuple (a.items()) '. Czy to gwarantuje sortowanie kluczy w tej samej kolejności dla wyraźnych, ale identycznych dyktatur? Słyszałem, że dyktura jest nieuporządkowana i polegając na takich rzeczach jak 'str ({1: 2,3: 4})' do tworzenia powtarzalnych łańcuchów przy identycznych danych wejściowych jest bardzo źle. – robru

+0

Wygląda na to, że 'inspect.getargspec (func) .args [0]' jest dokładną odpowiedzią na konkretne pytanie, które zadałem (jak znaleźć nazwę pierwszego argumentu), ale chciałbym rozwinąć to w bardziej ogólne rozwiązanie. Opublikuję wyniki później, gdy będę miał trochę czasu, aby to poprawić. – robru

+0

@Robru: dobry punkt na temat sortowania dyktowanego. Zmieniono na 'tuple (posortowane (a.items()))' (inną opcją byłoby 'frozenset (a.items())'). – georg

3

Spróbuj lru_cache:

@functools.lru_cache(maxsize=128, typed=False)

Dekorator zawinąć funkcję z memoizing wymagalne, że oszczędza się na MAXSIZE ostatnich połączeń. Może zaoszczędzić czas, gdy kosztowna lub związana funkcja I/O jest okresowo wywoływana z tymi samymi argumentami.

lru_cache dodany w Pythonie 3.2, ale mogą być przeniesione do 2.x

+0

Ciekawe czytać o, ale niestety nie działa w mojej sytuacji, bo mam klasy staticmethods, które muszą być w stanie iteracyjne nad pamięci podręcznej przypadkach tak samo cache musi być ujawnionym jako atrybut klasy. – robru

Powiązane problemy