2011-01-12 7 views
21

Używałem następującego dekoratora pamięci (z wielkiej książki Python Algorithms: Mastering Basic Algorithms w języku Python ... uwielbiam to, przy okazji).Python - czy ktoś ma dekorator do pamiętania, który poradzi sobie z nieodpartymi argumentami?

def memo(func): 
    cache = {} 
    @ wraps(func) 
    def wrap(*args): 
     if args not in cache: 
      cache[args] = func(*args) 
     return cache[args] 
    return wrap 

Problem z tym jest to, że dekorator cache słowniku oparte oznacza, że ​​wszystkie moje argumenty muszą być hashable.

Czy ktoś ma implementację (lub modyfikację tego), która dopuszcza niewzruszone argumenty (np. Słowniki)?

Wiem, że brak wartości hash oznacza, że ​​pytanie "czy to w pamięci podręcznej?" staje się nietrywialny, ale pomyślałem, że zapytam.

=== edytowane GIVE KONTEKST ===

pracuję nad funkcją, która zwraca Parnas stylu „używa hierarchii” dane słownikiem module: zależności. Oto konfiguracja:

def uses_hierarchy(requirements): 
    """ 
    uses_hierarchy(requirements) 

    Arguments: 
    requirements - a dictionary of the form {mod: list of dependencies, } 

    Return value: 
    A dictionary of the form {level: list of mods, ...} 

    Assumptions: 
    - No cyclical requirements (e.g. if a requires b, b cannot require a). 
    - Any dependency not listed as a mod assumed to be level 0. 

    """ 

    levels = dict([(mod, _level(mod, requirements)) 
        for mod in requirements.iterkeys()]) 
    reversed = dict([(value, []) for value in levels.itervalues()]) 
    for k, v in levels.iteritems(): 
     reversed[v].append(k) 
    return reversed 


def _level(mod, requirements): 
    if not requirements.has_key(mod): 
     return 0 
    dependencies = requirements[mod] 
    if not dependencies: 
     return 0 
    else: 
     return max([_level(dependency, requirements) 
        for dependency in dependencies]) + 1 

więc, że:

>>> requirements = {'a': [], 
...     'b': [], 
...     'c': ['a'], 
...     'd': ['a','b'], 
...     'e': ['c','d'], 
...     'f': ['e'] 
...     } 

>>> uses_hierarchy(requirements) 
{0: ['a', 'b'], 1: ['c', 'd'], 2: ['e'], 3: ['f']} 

_level jest funkcja Chcę memoize aby ta konfiguracja bardziej skalowalne. Wdrożone bez memoizacji oblicza poziom zależności wielokrotnie (np. "A" jest obliczane 8 razy, jak sądzę w powyższym przykładzie).

Dzięki,

Mike

+1

Cóż, po prostu je przechowywać w wykazie jako krotek '(args, wynik)' i iteracyjne nad nim. Jak jednak mówisz, nie będzie to szybkie. –

+1

@Thomas K: Pamięć podręczna, która jest wolniejsza, tym więcej rzeczy, które ma, brzmi naprawdę nieproduktywnie. –

+0

@ THC4k: To zależy od tego, czego chcesz. Jeśli masz zamiar uderzać w te same kilka możliwości wiele razy, a funkcja jest dużym obliczeniem lub powolnym żądaniem sieci, może to być więcej niż wystarczająco dobre. A na nowoczesnym komputerze może stać się całkiem duży, zanim stanie się problemem. –

Odpowiedz

14

Oto przykład w Alex Martelli Python Cookbook, które pokazują, jak stworzyć dekorator memoize korzystając cPickle dla funkcji, które miało zmienny argumentu (oryginalna wersja):

import cPickle 

class MemoizeMutable: 
    def __init__(self, fn): 
     self.fn = fn 
     self.memo = {} 
    def __call__(self, *args, **kwds): 
     import cPickle 
     str = cPickle.dumps(args, 1)+cPickle.dumps(kwds, 1) 
     if not self.memo.has_key(str): 
      print "miss" # DEBUG INFO 
      self.memo[str] = self.fn(*args, **kwds) 
     else: 
      print "hit" # DEBUG INFO 

     return self.memo[str] 

Oto link.

EDIT: Używając kodu, które zostały podane i ten memoize dekorator:

_level = MemoizeMutable(_level) 

equirements = {'a': [], 
       'b': [], 
       'c': ['a'], 
       'd': ['a','b'], 
       'e': ['c','d'], 
       'f': ['e'] 
       } 

print uses_hierarchy(equirements) 

udało mi się odtworzyć to:

miss 
miss 
hit 
miss 
miss 
hit 
miss 
hit 
hit 
hit 
miss 
hit 
{0: ['a', 'b'], 1: ['c', 'd'], 2: ['e'], 3: ['f']} 
+0

@MikeRand: Mam tylko edytowane moją odpowiedź ze swoim przykładzie, nadzieję, że może pomóc – mouad

+3

+1: Trawienie jest ciekawym sposobem hash zmienny obiektów. Sądzę, że to wygrałoby iterację listy dla wielu możliwości (ze względu na czas wyszukiwania), ale prostsze rozwiązanie ma mniej narzutów w kilku możliwych przypadkach. –

+0

Wydaje się działać, wielkie dzięki. Dla mojego zrozumienia: czy cPickle skutecznie serializuje argumenty, aby uczynić z serializacji odpowiedni klucz? – MikeRand

6

Technicznie można rozwiązać ten problem poprzez przekręcenie dict (lub list lub set) na krotki. Na przykład:

key = tuple(the_dict.iteritems()) 
key = tuple(the_list) 
key = tuple(sorted(the_set)) 

cache[key] = func(..) 

Ale nie byłoby to zrobić w memo, wolałbym zmienić funkcje, których chcesz używać notatkę na - na przykład, zamiast przyjąć do dict powinni przyjmować tylko (key, value) pary, zamiast pobierania list lub zestawów, które powinny po prostu wziąć *args.

+5

Należy również posortować pary kluczy i wartości słownika. – Ben

1

Nie mocno testowane, ale wydaje się działać:

from functools import wraps 

def memo(func): 
    cache = [] 
    argslist = [] 
    @ wraps(func) 
    def wrap(*args): 
     try: 
      result = cache[argslist.index(args)] 
      print 'cache hit' 
      return result 
     except ValueError: 
      argslist.append(args) 
      cache.append(func(*args)) 
      print 'cache miss' 
      return cache[-1] 
    return wrap 

d1 = { 'a':3, 'b':42 } 
d2 = { 'c':7, 'd':19 } 
d3 = { 'e':34, 'f':67 } 

@memo 
def func(d): 
    return sum(d.values()) 

print func(d1) 
# cache miss 
# 45 
print func(d2) 
# cache miss 
# 26 
print func(d3) 
# cache miss 
# 101 
print func(d2) 
# cache hit 
# 26 
+0

Powoduje wyświetlenie innej odpowiedzi niż oczekiwano. Po uruchomieniu na powyższym przykładzie wynikiem jest {0: ["a", "b"], 1: ["c"], 2: ["e", "d", "f"]}. Nie wiem, dlaczego „d” porusza się od poziomu 1 do poziomu 2. – MikeRand

+0

@MikeRand: Nie wiem - że może to być dlatego, że argumenty są przechowywane były zmienne, więc próbował podejmowania deepcopies z nich, ale to nie miało żadnego wpływu. Przyjrzymy się temu bardziej i damy znać i/lub zmienię moją odpowiedź. – martineau

2

Ponieważ nikt nie wspomniał o tym, P Ython Wiki ma bibliotekę dekoratorów, która zawiera numer memoizing decorator patterns.

Moje osobiste preferencje jest ostatnim, który umożliwia wywołanie kodu po prostu traktować jako metodę leniwie-ocenianej nieruchomości, a nie metody. Ale bardziej podoba mi się implementacja here.

class lazy_property(object): 
    '''Decorator: Enables the value of a property to be lazy-loaded. 
    From Mercurial's util.propertycache 

    Apply this decorator to a no-argument method of a class and you 
    will be able to access the result as a lazy-loaded class property. 
    The method becomes inaccessible, and the property isn't loaded 
    until the first time it's called. Repeated calls to the property 
    don't re-run the function. 

    This takes advantage of the override behavior of Descriptors - 
    __get__ is only called if an attribute with the same name does 
    not exist. By not setting __set__ this is a non-data descriptor, 
    and "If an instance's dictionary has an entry with the same name 
    as a non-data descriptor, the dictionary entry takes precedence." 
    - http://users.rcn.com/python/download/Descriptor.htm 

    To trigger a re-computation, 'del' the property - the value, not 
    this class, will be deleted, and the value will be restored upon 
    the next attempt to access the property. 
    ''' 
    def __init__(self,func): 
     self.func = func 
     self.name = func.__name__ 
    def __get__(self, obj, type=None): 
     result = self.func(obj) 
     setattr(obj, self.name, result) 
     return result 

W tym samym pliku połączonego powyżej tam Również lazy_dict dekorator, który pozwala traktować jako funkcję słownika z leniwie-ocenianych wartości.