2014-10-02 1 views
8

Sto lavorando in Python 2.7 e mi piace il problema che mi confonde.Perché impostare un metodo associato sull'oggetto python crea un riferimento circolare?

Questo è l'esempio più semplice:

>>> class A(object): 
    def __del__(self): 
     print("DEL") 
    def a(self): 
     pass 

>>> a = A() 
>>> del a 
DEL 

che è ok come previsto ... ora sto cercando di cambiare il metodo di oggetto aa() e ciò che accadrà è che dopo il cambiamento che non posso eliminare a più:

>>> a = A() 
>>> a.a = a.a 
>>> del a 

Solo per fare alcuni controlli ho stampare il riferimento a.a prima e dopo l'assegnazione

>>> a = A() 
>>> print a.a 
<bound method A.a of <__main__.A object at 0xe86110>> 
>>> a.a = a.a 
>>> print a.a 
<bound method A.a of <__main__.A object at 0xe86110>> 

Infine ho usato modulo objgraph per cercare di capire il motivo per cui l'oggetto non viene rilasciato:

>>> b = A() 
>>> import objgraph 
>>> objgraph.show_backrefs([b], filename='pre-backref-graph.png') 

pre-backref-graph.png

>>> b.a = b.a 
>>> objgraph.show_backrefs([b], filename='post-backref-graph.png') 

post-backref-graph.png

Come si può vedere nell'immagine la post-backref-graph.png lì è un riferimento a __self__ in b che non ha senso per me perché l'auto r le eferenze del metodo di istanza dovrebbero essere ignorate (come era prima del compito).

Qualcuno può spiegare perché questo comportamento e come posso aggirarlo?

risposta

3

Quando si scrive a.a, viene eseguito in modo efficace:

A.a.__get__(a, A) 

perché non si accede a un metodo di pre-bound, ma il metodo class' che si sta legato in fase di esecuzione.

Quando si esegue

a.a = a.a 

in modo efficace "cache" l'atto di legare il metodo. Poiché il metodo associato ha un riferimento all'oggetto (ovviamente, dato che deve passare self alla funzione) crea un riferimento circolare.


Quindi sono modellare il vostro problema come:

class A(object): 
    def __del__(self): 
     print("DEL") 
    def a(self): 
     pass 

def log_all_calls(function): 
    def inner(*args, **kwargs): 
     print("Calling {}".format(function)) 

     try: 
      return function(*args, **kwargs) 
     finally: 
      print("Called {}".format(function)) 

    return inner 

a = A() 
a.a = log_all_calls(a.a) 

a.a() 

È possibile utilizzare riferimenti deboli per legare su richiesta all'interno lof_all_calls come:

import weakref 

class A(object): 
    def __del__(self): 
     print("DEL") 
    def a(self): 
     pass 

def log_all_calls_weakmethod(method): 
    cls = method.im_class 
    func = method.im_func 
    instance_ref = weakref.ref(method.im_self) 
    del method 

    def inner(*args, **kwargs): 
     instance = instance_ref() 

     if instance is None: 
      raise ValueError("Cannot call weak decorator with dead instance") 

     function = func.__get__(instance, cls) 

     print("Calling {}".format(function)) 

     try: 
      return function(*args, **kwargs) 
     finally: 
      print("Called {}".format(function)) 

    return inner 

a = A() 
a.a = log_all_calls_weakmethod(a.a) 

a.a() 

Questo è veramente brutto, così ho preferirei estrarlo per creare un decoratore weakmethod:

import weakref 

def weakmethod(method): 
    cls = method.im_class 
    func = method.im_func 
    instance_ref = weakref.ref(method.im_self) 
    del method 

    def inner(*args, **kwargs): 
     instance = instance_ref() 

     if instance is None: 
      raise ValueError("Cannot call weak method with dead instance") 

     return func.__get__(instance, cls)(*args, **kwargs) 

    return inner 

class A(object): 
    def __del__(self): 
     print("DEL") 
    def a(self): 
     pass 

def log_all_calls(function): 
    def inner(*args, **kwargs): 
     print("Calling {}".format(function)) 

     try: 
      return function(*args, **kwargs) 
     finally: 
      print("Called {}".format(function)) 

    return inner 

a = A() 
a.a = log_all_calls(weakmethod(a.a)) 

a.a() 

Fatto!


FWIW, non solo Python 3.4 non ha questi problemi, ha anche WeakMethod pre-costruito per voi.

+0

Ok ... C'è un modo per evitarlo? Dovrei mettere in cache alcuni metodi e recuperare il metodo più tardi: è possibile? –

+0

Dipende da cosa stai cercando di fare. – Veedrac

+0

OK Ho trovato la soluzione: aa = types.MethodType (Aa, a, A) –

4

La risposta di Veedrac sul metodo associato che mantiene un riferimento all'istanza è solo una parte della risposta. garbage collector di CPython sa come rilevare e gestisce i riferimenti ciclici - tranne quando qualche oggetto che fa parte del ciclo ha un metodo __del__, come indicato qui https://docs.python.org/2/library/gc.html#gc.garbage:

oggetti che hanno __del__() metodi e fanno parte di un ciclo di riferimento causa l'indisponibilità dell'intero ciclo di riferimento, inclusi gli oggetti non necessariamente nel ciclo ma raggiungibili solo da esso. Python non raccoglie tali cicli automaticamente perché, in generale, , Python non è in grado di indovinare un ordine sicuro in cui eseguire i metodi __del__(). (...) In genere è meglio evitare il problema non creando cicli contenenti oggetti con i metodi __del__() e in questo caso i rifiuti possono essere esaminati in questo caso per verificare che nessuno di questi cicli sia stato creato .

IOW: rimuovere il metodo __del__ e si dovrebbe andare bene.

EDIT: WRT/il tuo commento:

lo uso per l'oggetto in funzione a.a = functor(a.a). Al termine del test , mi piacerebbe sostituire il funtore con il metodo originale.

allora la soluzione è chiaro e semplice:

a = A() 
a.a = functor(a.a) 
test(a) 
del a.a 

Fino a quando non esplicitamente legarla, a ha, quindi è lo sguardo sulla classe e non viene restituito alcun 'a' atribute esempio, una nuova method istanza (cf https://wiki.python.org/moin/FromFunctionToMethod per ulteriori informazioni su questo). Questa istanza method viene quindi chiamata e (solitamente) scartata.

+0

Questa non è una soluzione .... Ho bisogno del metodo __del__ (le ragioni di ciò sono fuori portata qui) e quel problema è l'unico che non so come risolvere per gli altri riferimenti deboli funziona bene –

+1

Bene , in realtà risponde alla tua domanda "Qualcuno può spiegare perché questo comportamento e come posso aggirarlo?" - non hai menzionato che hai bisogno di '__del__' e il tuo snippet non implica che sia di reale utilità;) Ora se guardi il link che ti ho indicato, c'è un po 'di più sull'argomento ... –

+0

Come scrivere nel link che hai postato il modo migliore è non creare ciclo e quello che ho chiesto è stato "non capisco perché quel ciclo nasce e se c'è un modo per non creare quel tipo di cicli" ... ma rimuovere __del__ non rimuovere il ciclo, ma solo gli effetti collaterali del ciclo :) –

1

Per quanto riguarda il motivo per cui Python esegue questa operazione. Tecnicamente tutti gli oggetti contengono riferimenti circolari se dispongono di metodi. Tuttavia, la garbage collection richiederebbe molto più tempo se il garbage collector dovesse fare controlli espliciti sui metodi degli oggetti per assicurarsi che la liberazione dell'oggetto non causasse un problema. Poiché tale Python memorizza i metodi separatamente da __dict__ di un oggetto. Quindi quando scrivi a.a = a.a, stai seguendo il metodo con se stesso nel campo a sull'oggetto. Quindi, esiste un riferimento esplicito al metodo che impedisce che l'oggetto venga liberato correttamente.

La soluzione al tuo problema non si preoccupa di mantenere una "cache" del metodo originale e basta eliminare la variabile ombreggiata quando hai finito. Ciò cancellerà il metodo e lo renderà nuovamente disponibile.

>>> class A(object): 
...  def __del__(self): 
...   print("del") 
...  def method(self): 
...   print("method") 
>>> a = A() 
>>> vars(a) 
{} 
>>> "method" in dir(a) 
True 
>>> a.method = a.method 
>>> vars(a) 
{'method': <bound method A.method of <__main__.A object at 0x0000000001F07940>>} 
>>> "method" in dir(a) 
True 
>>> a.method() 
method 
>>> del a.method 
>>> vars(a) 
{} 
>>> "method" in dir(a) 
True 
>>> a.method() 
method 
>>> del a 
del 

Qui vars mostra cosa c'è nella attributo __dict__ di un oggetto. Si noti come __dict__ non contiene un riferimento a se stesso anche se a.__dict__ è valido. dir produce un elenco di tutti gli attributi raggiungibili dall'oggetto specificato. Qui possiamo vedere tutti gli attributi e i metodi su un oggetto e tutti i metodi e gli attributi delle sue classi e delle loro basi.Ciò mostra che il metodo associato di a è memorizzato in luogo separato rispetto agli attributi di a memorizzati.

+0

Grazie per la risposta, ma la vera domanda non è" perché l'oggetto non è stato distrutto? " ma perché 'a.a = a.a' crea un riferimento circolare. Se rimuovi l'override '__del__' e provi a tracciare i grafici prima e dopo l'assegnazione, scoprirai che i grafici sono diversi e il secondo ha un riferimento circolare. –

+0

Ha frainteso la domanda. Ho completamente cambiato la mia risposta, con una nuova raccomandazione e ho imparato qualcosa io stesso nel processo. – Dunes

+0

THX: questo è esattamente ciò che intendevo con "Perché impostare un metodo associato a un oggetto python crea un riferimento circolare?". Veedrac mi ha dato un modo per lavorare con qualche problema correlato a questo, ma con la tua risposta lo capisco davvero. –