Qual è il modo pitonico di rilevare l'ultimo elemento in un ciclo 'for'?


188

Mi piacerebbe conoscere il modo migliore (il modo più compatto e "pitonico") per fare un trattamento speciale per l'ultimo elemento in un ciclo for. C'è un pezzo di codice che dovrebbe essere chiamato solo tra gli elementi, essere soppresso nell'ultimo.

Ecco come lo faccio attualmente:

for i, data in enumerate(data_list):
    code_that_is_done_for_every_element
    if i != len(data_list) - 1:
        code_that_is_done_between_elements

C'è un modo migliore?

Nota: non voglio farlo con hack come l'utilizzo reduce.;)


E il primo? Dovrebbe anche essere soppresso?
Adam Matan,

potresti dirci cosa si sta facendo tra gli elementi?
SilentGhost,

2
Vorrei ottenere la risposta per un caso generico, ma un caso concreto in cui ho bisogno di questo è scrivere cose su uno stream, con dei separatori tra loro, proprio come stream.write (',' .join (name_list)), ma farlo in un ciclo for senza concatenare le stringhe, perché ci sono molte scritture ...
e.tadeu,


Le prime tre righe di questa risposta mi hanno davvero aiutato, stava avendo una sfida simile.
cardamomo

Risposte:


151

La maggior parte delle volte è più semplice (ed economico) rendere la prima iterazione il caso speciale anziché l'ultimo:

first = True
for data in data_list:
    if first:
        first = False
    else:
        between_items()

    item()

Questo funzionerà per qualsiasi iterabile, anche per quelli che non hanno len():

file = open('/path/to/file')
for line in file:
    process_line(line)

    # No way of telling if this is the last line!

A parte questo, non penso che ci sia una soluzione generalmente superiore in quanto dipende da cosa stai cercando di fare. Ad esempio, se stai costruendo una stringa da un elenco, è naturalmente meglio usarlo str.join()che usare un forciclo "con un caso speciale".


Utilizzando lo stesso principio ma più compatto:

for i, line in enumerate(data_list):
    if i > 0:
        between_items()
    item()

Sembra familiare, vero? :)


Per @ofko e altri che hanno davvero bisogno di scoprire se il valore attuale di un iterabile senza len()è l'ultimo, dovrai guardare avanti:

def lookahead(iterable):
    """Pass through all values from the given iterable, augmented by the
    information if there are more values to come after the current one
    (True), or if it is the last value (False).
    """
    # Get an iterator and pull the first value.
    it = iter(iterable)
    last = next(it)
    # Run the iterator to exhaustion (starting from the second value).
    for val in it:
        # Report the *previous* value (more to come).
        yield last, True
        last = val
    # Report the last value.
    yield last, False

Quindi puoi usarlo in questo modo:

>>> for i, has_more in lookahead(range(3)):
...     print(i, has_more)
0 True
1 True
2 False

1
È vero, in questo modo sembra migliore del mio, almeno non è necessario usare enumerate e len.
e.tadeu,

Sì, ma ne aggiunge un altro ifche potrebbe essere evitato se il loop fosse diviso in due loop. Tuttavia, questo è rilevante solo quando si itera un enorme elenco di dati.
Adam Matan,

Il problema con la suddivisione in due loop è che viola il DRY o ti costringe a definire i metodi.
e.tadeu,

Cerco davvero di capire il tuo ultimo esempio (che funziona perfettamente nel mio codice), ma non capisco come funziona (l'idea alla base)
Olivier Pons

1
@OlivierPons Devi capire il protocollo iteratore di Python: ottengo un iteratore per un oggetto e recupero il primo valore con next(). Quindi sfrutto che un iteratore è iterabile da solo, quindi posso usarlo nel forciclo fino all'esaurimento, iterazione dal secondo all'ultimo valore. Durante questo, mantengo il valore corrente che ho recuperato dall'iteratore localmente e yieldl'ultimo invece. In questo modo so che c'è ancora un valore a venire. Dopo il ciclo for, ho riportato tutti i valori tranne l'ultimo.
Ferdinand Beyer,

20

Anche se questa domanda è piuttosto vecchia, sono venuto qui tramite Google e ho trovato un modo abbastanza semplice: elenco di sezioni. Supponiamo che tu voglia inserire un '&' tra tutte le voci dell'elenco.

s = ""
l = [1, 2, 3]
for i in l[:-1]:
    s = s + str(i) + ' & '
s = s + str(l[-1])

Questo restituisce '1 & 2 & 3'.


7
Hai appena reimplementato la funzione join: `" & ".join ([str (x) per x in l])
Bryan Oakley,

la concatenazione di stringhe è alquanto inefficiente. Se len(l)=1000000in questo esempio, il programma verrà eseguito per un po '. appendè raccomandato afaik. l=[1,2,3]; l.append(4);
plhn,

18

Il 'codice tra' è un esempio del modello Head-Tail .

Hai un oggetto, che è seguito da una sequenza di (tra, oggetto) coppie. Puoi anche vederlo come una sequenza di coppie (oggetto, tra) seguite da un oggetto. È generalmente più semplice prendere il primo elemento come speciale e tutti gli altri come il caso "standard".

Inoltre, per evitare la ripetizione del codice, è necessario fornire una funzione o un altro oggetto per contenere il codice che non si desidera ripetere. Incorporare un'istruzione if in un ciclo che è sempre falso, tranne una volta, è piuttosto sciocco.

def item_processing( item ):
    # *the common processing*

head_tail_iter = iter( someSequence )
head = head_tail_iter.next()
item_processing( head )
for item in head_tail_iter:
    # *the between processing*
    item_processing( item )

Questo è più affidabile perché è leggermente più facile da dimostrare, non crea una struttura di dati extra (cioè una copia di un elenco) e non richiede molta esecuzione sprecata di una condizione if che è sempre falsa tranne una volta.


4
Le chiamate di funzione sono molto più lente delle ifistruzioni, quindi l'argomento "esecuzione sprecata" non regge.
Ferdinand Beyer,

1
Non sono sicuro di quale sia la differenza di velocità tra la funzione chiamata e l'istruzione if abbia a che fare con qualcosa. Il punto è che questa formulazione non ha un'istruzione if che è sempre falsa (tranne una volta)
S. Lott

1
Ho interpretato la tua affermazione "... e non richiede molta esecuzione sprecata di una condizione if che è sempre falsa tranne una volta" come "... ed è più veloce poiché salva un paio di ifs". Ovviamente ti riferisci semplicemente alla "pulizia del codice"?
Ferdinand Beyer,

Definire una funzione invece di usare ifun'istruzione è davvero considerato più pulito dalla comunità Python?
Markus von Broady,

17

Se stai semplicemente cercando di modificare l'ultimo elemento data_list, puoi semplicemente usare la notazione:

L[-1]

Tuttavia, sembra che tu stia facendo di più. Non c'è niente di veramente sbagliato nel tuo cammino. Ho anche dato una rapida occhiata ad alcuni codici Django per i loro tag template e praticamente fanno quello che stai facendo.


1
Non lo sto modificando, lo sto usando per fare qualcosa
e.tadeu,

4
@ e.tadeu non importa nemmeno se lo stai modificando o meno. Cambiare la tua dichiarazione if in: if data != datalist[-1]:e mantenere tutto il resto uguale sarebbe il modo migliore per codificare questo secondo me.
spacetyper

8
@spacetyper Questo si interrompe quando l'ultimo valore non è univoco.
Ark-kun,

14

se gli articoli sono unici:

for x in list:
    #code
    if x == list[-1]:
        #code

altre opzioni:

pos = -1
for x in list:
    pos += 1
    #code
    if pos == len(list) - 1:
        #code


for x in list:
    #code
#code - e.g. print x


if len(list) > 0:
    for x in list[:-1]
        #code
    for x in list[-1]:
        #code

10

Questo è simile all'approccio di Ants Aasma ma senza usare il modulo itertools. È anche un iteratore in ritardo che guarda avanti un singolo elemento nel flusso iteratore:

def last_iter(it):
    # Ensure it's an iterator and get the first field
    it = iter(it)
    prev = next(it)
    for item in it:
        # Lag by one item so I know I'm not at the end
        yield 0, prev
        prev = item
    # Last item
    yield 1, prev

def test(data):
    result = list(last_iter(data))
    if not result:
        return
    if len(result) > 1:
        assert set(x[0] for x in result[:-1]) == set([0]), result
    assert result[-1][0] == 1

test([])
test([1])
test([1, 2])
test(range(5))
test(xrange(4))

for is_last, item in last_iter("Hi!"):
    print is_last, item

4

È possibile utilizzare una finestra scorrevole sopra i dati di input per dare un'occhiata al valore successivo e utilizzare una sentinella per rilevare l'ultimo valore. Funziona su qualsiasi iterabile, quindi non è necessario conoscere la lunghezza in anticipo. L'implementazione a coppie proviene da ricette itertools .

from itertools import tee, izip, chain

def pairwise(seq):
    a,b = tee(seq)
    next(b, None)
    return izip(a,b)

def annotated_last(seq):
    """Returns an iterable of pairs of input item and a boolean that show if
    the current item is the last item in the sequence."""
    MISSING = object()
    for current_item, next_item in pairwise(chain(seq, [MISSING])):
        yield current_item, next_item is MISSING:

for item, is_last_item in annotated_last(data_list):
    if is_last_item:
        # current item is the last item

3

Non c'è possibilità di iterare su tutto tranne l'ultimo elemento e trattare l'ultimo al di fuori del ciclo? Dopotutto, viene creato un ciclo per fare qualcosa di simile a tutti gli elementi su cui giri; se un elemento ha bisogno di qualcosa di speciale, non dovrebbe essere nel ciclo.

(vedi anche questa domanda: l'ultimo-elemento-in-un-ciclo-merita-un-trattamento separato )

EDIT: poiché la domanda riguarda più il "in Between", o il primo elemento è quello speciale in quanto non ha predecessore, o l' ultimo elemento è speciale in quanto non ha successori.


Ma l'ultimo elemento dovrebbe essere trattato in modo simile a qualsiasi altro elemento nell'elenco. Il problema è la cosa che dovrebbe essere fatta solo tra elementi.
e.tadeu,

In tal caso, il primo è l'unico senza un predecessore. Smontalo e fai un giro sul resto del codice generale dell'elenco.
xtofl,

3

Mi piace l'approccio di @ethan-t, ma while Trueè pericoloso dal mio punto di vista.

data_list = [1, 2, 3, 2, 1]  # sample data
L = list(data_list)  # destroy L instead of data_list
while L:
    e = L.pop(0)
    if L:
        print(f'process element {e}')
    else:
        print(f'process last element {e}')
del L

Qui, data_listè così che l'ultimo elemento è uguale per valore al primo dell'elenco. L può essere scambiato con, data_listma in questo caso risulta vuoto dopo il loop. while Trueè anche possibile utilizzarlo se si controlla che l'elenco non sia vuoto prima che l'elaborazione non sia necessaria (ouch!).

data_list = [1, 2, 3, 2, 1]
if data_list:
    while True:
        e = data_list.pop(0)
        if data_list:
            print(f'process element {e}')
        else:
            print(f'process last element {e}')
            break
else:
    print('list is empty')

La parte buona è che è veloce. Il cattivo: è distruttibile ( data_listdiventa vuoto).

La soluzione più intuitiva:

data_list = [1, 2, 3, 2, 1]  # sample data
for i, e in enumerate(data_list):
    if i != len(data_list) - 1:
        print(f'process element {e}')
    else:
        print(f'process last element {e}')

Oh sì, l'hai già proposto!


2

Non c'è niente di sbagliato nel tuo modo, a meno che tu non abbia 100.000 loop e desideri salvare 100.000 istruzioni "if". In tal caso, puoi procedere in questo modo:

iterable = [1,2,3] # Your date
iterator = iter(iterable) # get the data iterator

try :   # wrap all in a try / except
    while 1 : 
        item = iterator.next() 
        print item # put the "for loop" code here
except StopIteration, e : # make the process on the last element here
    print item

Uscite:

1
2
3
3

Ma davvero, nel tuo caso, ho la sensazione che sia eccessivo.

In ogni caso, probabilmente sarai più fortunato con l'affettare:

for item in iterable[:-1] :
    print item
print "last :", iterable[-1]

#outputs
1
2
last : 3

o semplicemente:

for item in iterable :
    print item
print iterable[-1]

#outputs
1
2
3
last : 3

Alla fine, un modo BACIO per farti cose, e che funzionerebbe con qualsiasi iterabile, compresi quelli senza __len__:

item = ''
for item in iterable :
    print item
print item

USCITE:

1
2
3
3

Se mi sento di farlo in questo modo, mi sembra semplice.


2
Ma nota che iterable [-1] non funzionerà con tutti gli iterables (come un generatore che non ha len )
e.tadeu,

Se tutto ciò che vuoi è accedere all'ultimo elemento dopo il ciclo, usa semplicemente iteminvece di ricalcolarlo usando list[-1]. Ma comunque: non credo sia questo ciò che l'OP stava chiedendo, vero?
Ferdinand Beyer,

Ri: iterable.__iter__() - per favore non chiamare __direttamente le funzioni. Dovrebbe essere iter(iterable).
PaulMcG,

2

Usa slicing e isper verificare l'ultimo elemento:

for data in data_list:
    <code_that_is_done_for_every_element>
    if not data is data_list[-1]:
        <code_that_is_done_between_elements>

Caveat emptor : funziona solo se tutti gli elementi nell'elenco sono effettivamente diversi (hanno posizioni diverse in memoria). Sotto il cofano, Python può rilevare elementi uguali e riutilizzare gli stessi oggetti per loro. Ad esempio, per stringhe dello stesso valore e numeri interi comuni.


2

se stai consultando l'elenco, anche per me ha funzionato:

for j in range(0, len(Array)):
    if len(Array) - j > 1:
        notLast()

2

Google mi ha portato a questa vecchia domanda e penso che potrei aggiungere un approccio diverso a questo problema.

La maggior parte delle risposte qui affronterebbe un corretto trattamento di un controllo loop for come è stato chiesto, ma se la data_list è distruttibile, suggerirei di estrarre gli elementi dall'elenco fino a quando non si finisce con un elenco vuoto:

while True:
    element = element_list.pop(0)
    do_this_for_all_elements()
    if not element:
        do_this_only_for_last_element()
        break
    do_this_for_all_elements_but_last()

potresti persino usare while len (element_list) se non hai bisogno di fare nulla con l'ultimo elemento. Trovo questa soluzione più elegante rispetto a next ().


2

Per me il modo più semplice e pitonico di gestire un caso speciale alla fine di un elenco è:

for data in data_list[:-1]:
    handle_element(data)
handle_special_element(data_list[-1])

Naturalmente questo può anche essere usato per trattare il primo elemento in modo speciale.


2

Invece di contare, puoi anche contare:

  nrToProcess = len(list)
  for s in list:
    s.doStuff()
    nrToProcess -= 1
    if nrToProcess==0:  # this is the last one
      s.doSpecialStuff()

1

Ritarda la gestione speciale dell'ultimo elemento fino a dopo il ciclo.

>>> for i in (1, 2, 3):
...     pass
...
>>> i
3

1

Possono esserci più modi. l'affettare sarà più veloce. Aggiungendo un altro che utilizza il metodo .index ():

>>> l1 = [1,5,2,3,5,1,7,43]                                                 
>>> [i for i in l1 if l1.index(i)+1==len(l1)]                               
[43]

0

Supponendo un input come iteratore, ecco un modo per usare tee e izip da itertools:

from itertools import tee, izip
items, between = tee(input_iterator, 2)  # Input must be an iterator.
first = items.next()
do_to_every_item(first)  # All "do to every" operations done to first item go here.
for i, b in izip(items, between):
    do_between_items(b)  # All "between" operations go here.
    do_to_every_item(i)  # All "do to every" operations go here.

demo:

>>> def do_every(x): print "E", x
...
>>> def do_between(x): print "B", x
...
>>> test_input = iter(range(5))
>>>
>>> from itertools import tee, izip
>>>
>>> items, between = tee(test_input, 2)
>>> first = items.next()
>>> do_every(first)
E 0
>>> for i,b in izip(items, between):
...     do_between(b)
...     do_every(i)
...
B 0
E 1
B 1
E 2
B 2
E 3
B 3
E 4
>>>

0

La soluzione più semplice che mi viene in mente è:

for item in data_list:
    try:
        print(new)
    except NameError: pass
    new = item
print('The last item: ' + str(new))

Quindi guardiamo sempre avanti un elemento ritardando l'elaborazione di una iterazione. Per evitare di fare qualcosa durante la prima iterazione, rilevo semplicemente l'errore.

Certo, devi pensare un po ', al fine di NameError che sia sollevato quando lo vuoi.

Conserva anche il `counstruct

try:
    new
except NameError: pass
else:
    # continue here if no error was raised

Ciò si basa sul fatto che il nome new non era precedentemente definito. Se sei paranoico puoi assicurarti che newnon esista usando:

try:
    del new
except NameError:
    pass

In alternativa puoi ovviamente usare anche un'istruzione if ( if notfirst: print(new) else: notfirst = True). Ma per quanto ne so il sovraccarico è più grande.


Using `timeit` yields:

    ...: try: new = 'test' 
    ...: except NameError: pass
    ...: 
100000000 loops, best of 3: 16.2 ns per loop

quindi mi aspetto che le spese generali non siano selezionabili.


0

Contare gli oggetti una volta e tenere il passo con il numero di elementi rimanenti:

remaining = len(data_list)
for data in data_list:
    code_that_is_done_for_every_element

    remaining -= 1
    if remaining:
        code_that_is_done_between_elements

In questo modo si valuta la lunghezza dell'elenco solo una volta. Molte delle soluzioni in questa pagina sembrano supporre che la lunghezza non sia disponibile in anticipo, ma questo non fa parte della tua domanda. Se hai la lunghezza, usala.


0

Una semplice soluzione che viene in mente sarebbe:

for i in MyList:
    # Check if 'i' is the last element in the list
    if i == MyList[-1]:
        # Do something different for the last
    else:
        # Do something for all other elements

Una seconda soluzione altrettanto semplice potrebbe essere ottenuta utilizzando un contatore:

# Count the no. of elements in the list
ListLength = len(MyList)
# Initialize a counter
count = 0

for i in MyList:
    # increment counter
    count += 1
    # Check if 'i' is the last element in the list
    # by using the counter
    if count == ListLength:
        # Do something different for the last
    else:
        # Do something for all other elements
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.