2015-08-18 10 views
10

Il contesto originale di questo bug è un pezzo di codice troppo grande per essere inserito in una domanda come questa. Ho dovuto ridurre questo codice a uno snippet minimo che mostra ancora il bug. Questo è il motivo per cui il codice mostrato di seguito è un po 'bizzarro.Fastidioso bug del generatore

Nel codice seguente, la classe Foo può essere considerata un modo complesso per ottenere qualcosa come xrange.

class Foo(object): 
    def __init__(self, n): 
     self.generator = (x for x in range(n)) 

    def __iter__(self): 
     for e in self.generator: 
      yield e 

Infatti, Foo sembra comportarsi molto simile xrange:

for c in Foo(3): 
    print c 
# 0 
# 1 
# 2 

print list(Foo(3)) 
# [0, 1, 2] 

Ora, la sottoclasse Bar di Foo aggiunge solo un metodo __len__:

class Bar(Foo): 
    def __len__(self): 
     return sum(1 for _ in self.generator) 

Bar comporta come Foo se utilizzato in un for -loop:

for c in Bar(3): 
    print c 
# 0 
# 1 
# 2 

MA:

print list(Bar(3)) 
# [] 

mia ipotesi è che, nella valutazione della list(Bar(3)), il metodo di Bar(3)__len__ viene sempre chiamato, usando così il generatore.

(Se questa ipotesi è corretta, la chiamata a Bar(3).__len__ è necessario, dopo tutto, list(Foo(3)) produce il risultato corretto anche se Foo non ha un metodo __len__.)

Questa situazione è fastidioso: non c'è nessuna buona ragione per list(Foo(3)) e list(Bar(3)) per produrre risultati diversi.

E 'possibile fissare Bar (senza, ovviamente, per liberarsi del suo metodo di __len__) in modo tale che i rendimenti list(Bar(3))[0, 1, 2]?

+1

Che cosa succede se il generatore è infinito? – thefourtheye

+0

@thefourtheye: poiché, AFAICT, lo scenario che proponi non può accadere nel codice presentato, non so come interpretare la tua domanda. – kjo

+0

Hai ragione che 'list (Bar (3))' chiama '__len__' (che puoi vedere semplicemente aggiungendo un'istruzione print al tuo metodo len). La mia domanda è: sai che i generatori possono essere esauriti e dovrebbero essere usati solo una volta, quindi invece di memorizzare l'oggetto generatore stesso, perché non progettare la tua classe per sapere come generare uno_ su richiesta? –

risposta

6

Il tuo problema è che Foo non si comporta allo stesso modo di xrange: xrange ti dà un nuovo iteratore ogni volta che chiedi il suo metodo iter, mentre Foo ti dà sempre lo stesso, il che significa che una volta esaurito anche l'oggetto:

>>> a = Foo(3) 
>>> list(a) 
[0, 1, 2] 
>>> list(a) 
[] 
>>> a = range(3) 
>>> list(a) 
[0, 1, 2] 
>>> list(a) 
[0, 1, 2] 

ho potuto facilmente confermare che il metodo viene chiamato dal __len__list aggiungendo spys ai vostri metodi:

class Bar(Foo): 
    def __len__(self): 
     print "LEN" 
     return sum(1 for _ in self.generator) 

(e ho aggiunto una print "ITERATOR" a 012.).Produce:

>>> list(Bar(3)) 
LEN 
ITERATOR 
[] 

Posso solo immaginare due soluzioni:

  1. il mio preferito uno: tornare un nuovo iteratore a ogni chiamata a __iter__ a Foo livello di imitare xrange:

    class Foo(object): 
        def __init__(self, n): 
         self.n = n 
    
        def __iter__(self): 
         print "ITERATOR" 
         return (x for x in range(self.n)) 
    
    class Bar(Foo): 
        def __len__(self): 
         print "LEN" 
         return sum(1 for _ in self.generator) 
    

    abbiamo ottenuto correttamente:

    >>> list(Bar(3)) 
    ITERATOR 
    LEN 
    ITERATOR 
    [0, 1, 2] 
    
  2. l'alternativa: Len cambiamento di non chiamare l'iteratore e lasciare Foo intatta:

    class Bar(Foo): 
        def __init__(self, n): 
         self.len = n 
         super(Bar, self).__init__(n) 
        def __len__(self): 
         print "LEN" 
         return self.len 
    

    Anche in questo caso otteniamo:

    >>> list(Bar(3)) 
    LEN 
    ITERATOR 
    [0, 1, 2] 
    

    ma Foo e Bar oggetti sono esaurite una volta primi raggiunge iteratore la sua fine

Ma devo ammettere che io non conosco il contesto delle classi reali ...

2

Questo comportamento potrebbe essere fastidioso, ma in realtà è abbastanza comprensibile. Internamente un list è semplicemente un array e un array è un datastructure di dimensioni fisse. Il risultato di ciò è che se si dispone di un n e si desidera aggiungere un elemento aggiuntivo per raggiungere n+1, sarà necessario creare un nuovo array completamente nuovo e copiare quello vecchio in quello nuovo. In effetti il ​​tuo list.append(x) è ora un operazione O(n) invece del normale O(1).

Per evitare ciò, list() tenta di ottenere la dimensione dell'input in modo che possa indovinare quale dimensione deve essere la matrice.

Così una soluzione per questo problema è quello di costringerlo a intuire utilizzando iter:

list(iter(Bar(3))) 
Problemi correlati