5

cercando di modificare un non decoratore utilizzare un weakref, mi sono imbattuto nel seguente comportamento:Perdita di memoria quando si richiama __iadd__ via __get__ senza l'utilizzo temporaneo

import weakref 

class descriptor(object): 
    def __get__(self, instance, owner): 
     return proxy(instance) 

class proxy(object): 
    def __init__(self, instance): 
     self.instance = instance 

    def __iadd__(self, other): 
     return self 

class A(object): 
    descr = descriptor() 

def is_leaky(test_fn): 
    a = A() 
    wr = weakref.ref(a) 
    test_fn(a) 
    del a 
    return wr() is not None 

def test1(a): 
    tmp = a.descr 
    tmp += object() 

def test2(a): 
    a.descr += object() 

print(is_leaky(test1)) # gives False 
print(is_leaky(test2)) # gives True!!! 

Questo sembra molto strano per me, come avevo aspettarsi che entrambi i casi si comportino allo stesso modo. Inoltre, dalla mia comprensione del conteggio dei riferimenti e della durata dell'oggetto, ero convinto che in entrambi i casi l'oggetto dovrebbe essere liberato.

L'ho provato sia su python2.7 che su python3.3.

Si tratta di un bug o di un comportamento intenzionale? C'è un modo per ottenere che entrambe le chiamate abbiano i risultati attesi (liberare l'oggetto in questione)?

Non voglio usare un weakref in proxy perché questo distrugge la corretta semantica di oggetti a vita per i metodi bound:

a = A() 
descr = a.descr 
del a # a is kept alive since descr is a bound method to a 
descr() # should execute a.descr() as expected 

risposta

5

I due codepaths non sono equivalenti.

Aggiunta sul posto agisce su due operatori, il target di assegnazione e l'elemento aggiunto. In test1 cioè temp, una variabile locale, e il sul posto Oltre è tradotto seguente:

temp = temp.__iadd__(object()) 

e poiché si torna self e temp si riferisce allo stesso oggetto, questo diventa temp = temp e il riferimento viene pulita dopo la chiusura della funzione.

In test2, voi le cose complicate, dato che ora i descrittori sono coinvolti ancora:

a.descr += object() 

diventa:

a.descr = A.__dict__['descr'].__get__(a, A).__iadd__(object()) 

in modo da assegnare il risultato di A.__dict__['descr'].__get__(a, A) all'istanza attribuire a.descr; il descrittore non ha il metodo __set__() e non viene consultato.

Ma, e qui è il fermo, l'oggetto proxy contiene un riferimento a a sé, a.descr.instance è un riferimento a a! Hai creato un riferimento circolare.

Questo riferimento mantiene l'oggetto in vita abbastanza a lungo da mostrare attraverso il riferimento debole, ma non appena il processo di garbage collection viene eseguito e interrompe questo ciclo, lo a verrà rimosso.

Morale di questa storia? Non utilizzare __iadd__ in combinazione con un descrittore non di dati; includere sia __get__e__set__ perché è necessario controllare cosa succede quando il risultato viene assegnato indietro.

+0

Infatti, aggiungendo 'gc.collect()' a 'is_leaky' interrompe il riferimento circolare e causa' is_leaky (test2) 'per restituire False. – unutbu

+0

Sì, cambiando test2 per avere a.descr .__ iadd __ (oggetto()) risulta un secondo Falso. – BartoszKP

+1

WOW. Questo è sottile. L'aggiunta di un NOP '__set__' al descrittore risolve il problema. Grazie mille! – coldfix