2015-04-29 11 views
6

Ho un semplice memoizer che sto usando per risparmiare un po 'di tempo per le chiamate di rete costose. Approssimativamente, il mio codice simile a questo:Come possono essere testate le funzioni memoizzate?

# mem.py 
import functools 
import time 


def memoize(fn): 
    """ 
    Decorate a function so that it results are cached in memory. 

    >>> import random 
    >>> random.seed(0) 
    >>> f = lambda x: random.randint(0, 10) 
    >>> [f(1) for _ in range(10)] 
    [9, 8, 4, 2, 5, 4, 8, 3, 5, 6] 
    >>> [f(2) for _ in range(10)] 
    [9, 5, 3, 8, 6, 2, 10, 10, 8, 9] 
    >>> g = memoize(f) 
    >>> [g(1) for _ in range(10)] 
    [3, 3, 3, 3, 3, 3, 3, 3, 3, 3] 
    >>> [g(2) for _ in range(10)] 
    [8, 8, 8, 8, 8, 8, 8, 8, 8, 8] 
    """ 
    cache = {} 

    @functools.wraps(fn) 
    def wrapped(*args, **kwargs): 
     key = args, tuple(sorted(kwargs)) 
     try: 
      return cache[key] 
     except KeyError: 
      cache[key] = fn(*args, **kwargs) 
      return cache[key] 
    return wrapped 


def network_call(user_id): 
    time.sleep(1) 
    return 1 


@memoize 
def search(user_id): 
    response = network_call(user_id) 
    # do stuff to response 
    return response 

E ho le prove per questo codice, dove ho un mock di valori di ritorno di diversi network_call() per assicurarsi che alcune modifiche che faccio in search() lavoro come previsto.

import mock 

import mem 


@mock.patch('mem.network_call') 
def test_search(mock_network_call): 
    mock_network_call.return_value = 2 
    assert mem.search(1) == 2 


@mock.patch('mem.network_call') 
def test_search_2(mock_network_call): 
    mock_network_call.return_value = 3 
    assert mem.search(1) == 3 

Tuttavia, quando ho eseguito questi test, ottengo un fallimento, perché search() restituisce un risultato in cache.

CAESAR-BAUTISTA:~ caesarbautista$ py.test test_mem.py 
============================= test session starts ============================== 
platform darwin -- Python 2.7.8 -- py-1.4.26 -- pytest-2.6.4 
collected 2 items 

test_mem.py .F 

=================================== FAILURES =================================== 
________________________________ test_search_2 _________________________________ 

args = (<MagicMock name='network_call' id='4438999312'>,), keywargs = {} 
extra_args = [<MagicMock name='network_call' id='4438999312'>] 
entered_patchers = [<mock._patch object at 0x108913dd0>] 
exc_info = (<class '_pytest.assertion.reinterpret.AssertionError'>, AssertionError(u'assert 2 == 3\n + where 2 = <function search at 0x10893f848>(1)\n + where <function search at 0x10893f848> = mem.search',), <traceback object at 0x1089502d8>) 
patching = <mock._patch object at 0x108913dd0> 
arg = <MagicMock name='network_call' id='4438999312'> 

    @wraps(func) 
    def patched(*args, **keywargs): 
     # don't use a with here (backwards compatability with Python 2.4) 
     extra_args = [] 
     entered_patchers = [] 

     # can't use try...except...finally because of Python 2.4 
     # compatibility 
     exc_info = tuple() 
     try: 
      try: 
       for patching in patched.patchings: 
        arg = patching.__enter__() 
        entered_patchers.append(patching) 
        if patching.attribute_name is not None: 
         keywargs.update(arg) 
        elif patching.new is DEFAULT: 
         extra_args.append(arg) 

       args += tuple(extra_args) 
>    return func(*args, **keywargs) 

/opt/boxen/homebrew/lib/python2.7/site-packages/mock.py:1201: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

mock_network_call = <MagicMock name='network_call' id='4438999312'> 

    @mock.patch('mem.network_call') 
    def test_search_2(mock_network_call): 
     mock_network_call.return_value = 3 
>  assert mem.search(1) == 3 
E  assert 2 == 3 
E  + where 2 = <function search at 0x10893f848>(1) 
E  + where <function search at 0x10893f848> = mem.search 

test_mem.py:15: AssertionError 
====================== 1 failed, 1 passed in 0.03 seconds ====================== 

C'è un modo per testare le funzioni memorizzate? Ho preso in considerazione alcune alternative, ma ognuna ha degli svantaggi.

Una soluzione è prendere in giro memoize(). Sono riluttante a farlo perché fa filtrare i dettagli di implementazione ai test. In teoria, dovrei essere in grado di memoizzare e disimitare le funzioni senza il resto del sistema, compresi i test, rilevando da un punto di vista funzionale.

Un'altra soluzione è riscrivere il codice per esporre la funzione decorata. Cioè, ho potuto fare qualcosa di simile:

def _search(user_id): 
    return network_call(user_id) 
search = memoize(_search) 

Tuttavia, questo corre negli stessi problemi di cui sopra, anche se è forse peggio, perché non funziona per funzioni ricorsive.

+3

Non sono sicuro di aver capito. Perché hai due test che testano valori di ritorno diversi? Se è corretto per la funzione memorizzata restituire un valore "obsoleto" (non lo stesso del valore live dalla rete), non è necessario testare due valori. Se non va bene, è necessario rendere la tua memoizzazione più sofisticata in modo da poter in qualche modo invalidare la cache quando necessario. Non è utile in memoizing se non si ha un modo per sapere quando va bene usare il valore memoized e ottenere il valore reale. – BrenBarn

+0

Non capisco perché la soluzione proposta non funzionerebbe. Non potresti usare '_search' nei tuoi test? La mia ipotesi è che si voglia testare il comportamento di '_search' quando la chiamata di rete restituisce vari valori e non è interessata alla memoizzazione. –

+0

Hm, avendo problemi a seguirti. I test hanno valori di ritorno diversi per simulare 'network_call()' i cui valori di ritorno possono essere diversi per gli stessi parametri (ad esempio in base al valore su un server). Dovrebbero essere indipendenti, quindi non dovrebbe esserci la necessità di invalidare la cache. –

risposta

8

È davvero auspicabile che la memoizzazione sia definita a livello di funzione?

Ciò rende effettivamente i dati memorizzati una variabile globale (proprio come la funzione, di cui condivide l'ambito).

Per inciso, è per questo che hai difficoltà a provarlo!

Quindi, che ne dici di avvolgere questo in un oggetto?

import functools 
import time 

def memoize(meth): 
    @functools.wraps(meth) 
    def wrapped(self, *args, **kwargs): 

     # Prepare and get reference to cache 
     attr = "_memo_{0}".format(meth.__name__) 
     if not hasattr(self, attr): 
      setattr(self, attr, {}) 
     cache = getattr(self, attr) 

     # Actual caching 
     key = args, tuple(sorted(kwargs)) 
     try: 
      return cache[key] 
     except KeyError: 
      cache[key] = meth(self, *args, **kwargs) 
      return cache[key] 

    return wrapped 

def network_call(user_id): 
    print "Was called with: %s" % user_id 
    return 1 

class NetworkEngine(object): 

    @memoize 
    def search(self, user_id): 
     return network_call(user_id) 


if __name__ == "__main__": 
    e = NetworkEngine() 
    for v in [1,1,2]: 
     e.search(v) 
    NetworkEngine().search(1) 

Resa:

Was called with: 1 
Was called with: 2 
Was called with: 1 

In altre parole, ogni istanza di NetworkEngine ottiene la propria cache. Basta riutilizzare lo stesso per condividere una cache, o istanziarne uno nuovo per ottenere una nuova cache.


Nel codice di prova, utilizza:

@mock.patch('mem.network_call') 
def test_search(mock_network_call): 
    mock_network_call.return_value = 2 
    assert mem.NetworkEngine().search(1) == 2 
+1

+1 per un'ottima risposta, ma potrebbe essere un po 'più veloce da capire se si è utilizzato il codice originale dell'OP. –

+1

@Asad Questo è un buon punto! Aggiornato:) –

0

Si consiglia di verificare ogni preoccupazione a parte:

Hai dimostrato memoize, e presumo aver testato questo.

Sembra che tu abbia network_call, quindi dovresti testarlo da solo, non memoizzato.

Ora si desidera combinare i due, ma presumibilmente questo sarà a vantaggio di un altro codice per evitare una latenza di rete prolungata. Tuttavia, se si desidera testare questo altro codice, non dovrebbe nemmeno effettuare 1 chiamata di rete, quindi potrebbe essere necessario fornire un nome di funzione come parametro.

Problemi correlati