Newsgroup: it.comp.lang
Titolo: [PYTHON] Itaratori bidirezionali

Un po' di tempo fa (forse prima dell'usicta di Python 2.2) avevo chiesto 
qui o su free.it.python le motivazioni per le quali gli iteratori non sono 
bidirezionali in Python.
Me le ricordo solo vagamente e google non ha tenuto traccia di quei 
messaggi.
Quali sono oltre il dover valutare attentamente la questione prima di fare 
errori irreparabili?
Invece di obbligare gli itearatori ad implementare in qualche modo un 
metodo "prev", non sarebbe meglio introdurre nella libreria di Python una 
classe che crei un iteratore bidirezionale a partire da uno 
mododirezionale?
Io ho provato a scriverne una (non l'ho testata). Ha qualche problema 
(oltre a non essere thread-safe)?
class BidirIterator(object):
    def __init__(self, *args, **kwargs):
        self._it = iter(*args, **kwargs)
        self._prev_ls = []
        self._next_ls = []
        self.use_padding = False
        self.pad_value = None
    def __iter__(self):
        return self
    def next(self):
        if self._next_ls:
            ret = self._next_ls.pop()
        else:
            ret = self._it.next()
        self._prev_ls.append(ret)
        return ret
    def prev(self):
        if not self._prev_ls:
            if self.use_padding:
                return self.pad_value
            else:
                raise 'BHO' # Che eccezione dovrei lanciare? IndexError?
        ret = self._prev_ls.pop()
        self._next_ls.append(ret)
        return ret
    def rewind(self):
        self._next_ls += self._prev_ls
        self._prev_ls = []
    def _set_use_padding(self, value):
        self._use_padding = bool(value)
    def _get_use_padding(self):
        return self._use_padding
    use_padding = property(_get_use_padding, _set_use_padding)
-- 
Marco Barisione
http://spazioinwind.libero.it/marcobari/

 iteratori non sono bidirezionali in Python.
azzardo una risposta: creerebbe dei problemi con i generatori.
Anche la classe che hai scritto tu IMHO non funziona come dovrebbe: 
se ho un iteratore derivato da un generatore mi aspetto che quando chiamo 
next() venga eseguito del codice sino al prossimo yield, mentre con la 
tua classe questo dipende dallo stato in cui si trova il 
simil-generatore.
Tra l'altro mi sembra che lo scopo primario degli iteratori sia di essere 
usato in congiunzione con il ciclo for, quindi un metodo prev sarebbe 
poco utile in quel contesto; quali sarebbero i vantaggi di un iteratore 
bidirezionale?
ciao Anto
-- 
"Computer science is not about computers any more than astronomy
is about telescopes." -- EW Dijkstra

 bidirezionale?
Mi è capitato alcune volte di aver bisogno di iteratori bidirezionali, 
anche se adesso non mi vengono esempi concreti. Ovviamente non avrebbe 
molto senso fare:
for elem in BidirIterator(sequenza):
    # ...


 gią stato generato in precedenza.
appunto: in pratica "tieni in cache" i valori gia` generati: questo 
significa che a volte una chiamata a next() sul bidirezionale non 
implica una chiamata a next() sull'iteratore sottostante.
Se ho un generatore *so* che ogni volta che chiamo next() viene eseguito 
del codice sino al prossimo yield; con la tua classe questa assunzione 
non e` piu` vera.


 non e` piu` vera.
È questo lo scopo dell'iteratore bidirezionale, tornare indietro nrei 
valori già generati. Tu hai un'idea diversa di come dovrebbe comportarsi?
Al massimo si può modificare affinché usi il prev dell'iteratore se questo 
metodo esiste, cioè qualcosa come (il codice non è testato e potrebbe 
essere pieno di bug):
    def __init__(self, *args):
        assert args
    	if type(args[0]) == types.GeneratorType:
    	    self._it = args[0]
    	    assert len(args) == 1
    	else:
            self._it = iter(*args)
        if hasattr(self._it, 'prev') and callable(self._it.prev):
            self.prev = self._it.prev
            self.next = self._it.next
        else:
            self._prev_ls = []
            self._next_ls = []
        # Le prossime due valiabili non avrebbero più effetto.
        self.use_padding = False
        self.pad_value = None


 comportarsi?
no, non ho nessuna idea :-)
Solo che mi sembrava un difetto e volevo fartelo notare.


 Solo che mi sembrava un difetto e volevo fartelo notare.
Bho, ci penserò, mi piacerebbe che mi rispondesse anche Alex Martelli, al 
massimo rispedisco il messaggio sulla mailing list.


 massimo rispedisco il messaggio sulla mailing list.
No, no, seguo anche qua, e` che non capivo bene le ragioni della
discussione, specificamente delle obiezioni di Antonio.  Se non ho
capito male, Antonio obietta che, se G e` un generatore e g=G(),
ogni chiamata a g.next() [sino a quando non si solleva una
StopIteration] esegue codice in G; mentre, se h = BDiter(G()),
non e` detto che una data chiamata a h.next() esegua codice in
G.  E la mia reazione e`, "e allora"?  Esattamente lo stesso si
avrebbe se j = iter(list(G())) -- una chiamata a j.next() non
esegue codice in G -- il codice e` gia` sempre stato eseguito
prima, in fase di costruzione di j, mentre nel caso di h alcune
chiamata a h.next() eseguiranno codice in G, altre (quelle che
sono state precedute da chiamate a h.prev()) no.  Perche` e` un
difetto?  E` una _caratteristica_ come lo e` quella di j.
Non conosco casi d'uso in cui la disponibilita` di next e prev
importi ma non ci si possa permettere di costruire una lista e
usare quella -- posso naturalmente concepirne dei teorici, ma
non mi e` mai successo in pratica.  Ho invece casi d'uso per
un iteratore *con push-back*, questione un poco diversa:
class IterPushback(object):
    def __init__(self, it):
 
self.it
 = it
        self.pb = []
    def pushback(self, x):
        self.pb.append(x)
    def next(self):
        try: return self.pb.pop()
        except: return self.it.next()
    def __iter__(self):
        return self
Anche qui, se k = IterPushback(G()), non e` detto che k.next()
esegua codice in G (potrebbe invece ritornare uno dei valori
sulla lista di pushback).
Un tipico esempio di uso di un iteratore con pushback e`
quello in cui capita di dovere "consumare tutte le prossime
entry che sono None" lasciando l'iteratore nello stato in
cui la prossima next() sara` non-None (o sollevera` la
StopIteration).  Con pushback:
    for x in pbiter:
        if x is not None:
            pbiter.pushback(x)
            break
mentre con iteratore bidirezionale si avrebbe similmente:
    for x in bditer:
        if x is not None:
            bditer.prev()
            break
Ma l'iteratore con pushback e` piu` leggero (tipicamente
la lista dei pushback e` vuota o comunque piccola, mentre
le liste del bidirezionale crescono senza limiti) e offre
inoltre la possibilita` di effettuare "editing" sull'iterazione
da parte di segmenti del codice-cliente che eseguono il
pushback di elementi che non si sono davvero presentati.
Ma se a uno serve il bidirezionale senza potersi permettere
un'intera list di tutti i risultati, beh, se serve, serve!-)
Alex

 non mi e` mai successo in pratica.
Esempi che mi vengono in mente in cui non si può usare una lista:
- La lista sarebbe molto lunga (o addirittura infinita)
Spesso si vede codice come:
i = 0
while True:
    # codice
    if some_condition:
        break
    # codice
    i += 1
Non mi piace molto perché, regolarmente, mi dimentico l'incremento e 
l'inizializzazione, quindi uso:
for i in infrange():
    # codice
    if some_condition:
        break
    # codice
Dove infrange è:
def infrange(start=0, step=1):
    while True:
        yield start
        start += step
Ovviamente list(infrange()) non è possibile.
- Creare subito la lista non è possibile perché ogni next ritorna un valore 
che può dipendere da altre condizioni esterne.
- Creare subito la lista blocca il programma.
Ad esempio itero su un oggetto che sta leggendo dei dati da un socket e me 
li ritorna in qualche forma, se tento di trasformarlo subito in lista devo 
aspettare che tutti i dati siano stati trasferiti. Al contrario potrei 
iniziare ad elaborarli subito man mano che mi arrivano.
Questo esempio vale anche per il caso precedente, cioè ciò che mi arriva 
sul socket può dipendere da ciò che ho inviato sul socket.
A questo punto rimane da vedere in quanti casi serva davvero tornare 
indietro.
Si può aggiungere un metodo del_history che cancelli la lista creata dai 
prev per evitare liste troppo lunghe, ma la necessità di dover chiamare 
questo metodo farebbe perdere l'utilità e la facilità d'uso dell'iteratore 
bidirezionale.
Dici che ha senso mandare l'iteratore bidirezionale sul cookbook nel sito 
ActiveState? O è troppo semplice/di scarso uso?


 ActiveState? O è troppo semplice/di scarso uso?
Male non puo` fare, basta aggiungere tutte le considerazioni che
ci siamo scambiati su di esso in questo thread e magari le varie
alternative come il pushback.
L'alternativa principale nel caso in cui .next() "dipende da altre
condizioni esterne" e` mantenere la storia in modo SEPARATO
dall'attualita`: ogni .next() e` esattamente il .next() del
dato iteratore wrappato, la lista storica (eventualmente con
una finestra limitata in un modo o in un altro) si accede invece
con metodi separati (o come semplice attributo-lista).  Ad es:
class WithHistory(object):
    def __init__(self, it):
 
self.it
 = it
        self.history = []
    def __iter__(self):
        return self
    def next(self):
        self.history.append(self.it.next())
        return self.history[-1]
class WithBoundedHistory(WithHistory):
    def __init__(self, it, bound):
        self.bound = bound
        super(WithBoundedHistory, self).__init__(it)
    def next(self):
        del self.history[-self.bound:]
        return super(WithBoundedHistory, self).next()
e cosi` via.
Alex