¿Cuál es la forma pitónica de detectar el último elemento en un bucle 'for'?

188

Me gustaría saber la mejor manera (más compacta y "pitónica") de hacer un tratamiento especial para el último elemento en un bucle for. Hay un código que debe llamarse solo entre elementos, que se suprime en el último.

Así es como lo hago actualmente:

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

¿Hay alguna forma mejor?

Nota: no quiero hacerlo con hacks como el uso reduce.;)

e.tadeu
fuente
¿Qué pasa con el primero? ¿Debería ser suprimido también?
Adam Matan
¿podría decirnos qué se está haciendo entre los elementos?
SilentGhost
2
Me gustaría obtener la respuesta para un caso genérico, pero un caso concreto donde necesito esto es escribir cosas en una secuencia, con separadores entre ellas, al igual que stream.write (',' .join (name_list)), pero hacerlo en un bucle sin concatenar las cadenas, porque hay muchas escrituras ...
e.tadeu
Las primeras tres líneas de esta respuesta realmente me ayudaron, tenía un desafío similar.
cardamomo

Respuestas:

151

La mayoría de las veces es más fácil (y más barato) hacer que la primera iteración sea el caso especial en lugar del último:

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

    item()

Esto funcionará para cualquier iterable, incluso para aquellos que no tienen len():

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

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

Aparte de eso, no creo que haya una solución generalmente superior, ya que depende de lo que intente hacer. Por ejemplo, si está creando una cadena a partir de una lista, es naturalmente mejor usar str.join()que usar un forbucle "con mayúsculas y minúsculas".


Usando el mismo principio pero más compacto:

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

Parece familiar, ¿no? :)


Para @ofko, y otros que realmente necesitan averiguar si el valor actual de un iterable sin len()es el último, deberá mirar hacia adelante:

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

Entonces puedes usarlo así:

>>> for i, has_more in lookahead(range(3)):
...     print(i, has_more)
0 True
1 True
2 False
Fernando Beyer
fuente
1
Es cierto, esta forma parece mejor que la mía, al menos no necesita usar enumerate y len.
e.tadeu
Sí, pero agrega otro ifque podría evitarse si el bucle se dividiera en dos bucles. Sin embargo, esto es relevante solo cuando se itera una gran lista de datos.
Adam Matan el
El problema de dividirse en dos bucles es que viola DRY o lo obliga a definir métodos.
e.tadeu
Realmente trato de entender tu último ejemplo (que funciona perfectamente en mi código), pero no entiendo cómo funciona (la idea detrás)
Olivier Pons
1
@OlivierPons Debe comprender el protocolo iterador de Python: obtengo un iterador para un objeto y recupero el primer valor con next(). Luego exploto que un iterador es iterable por sí mismo, por lo que puedo usarlo en el forciclo hasta el agotamiento, la iteración del segundo al último valor. Durante esto, mantengo el valor actual que recuperé del iterador localmente y yieldel último en su lugar. De esta manera sé que hay un valor más por venir. Después del ciclo for, he informado todos los valores, excepto el último.
Ferdinand Beyer
20

Aunque esa pregunta es bastante antigua, vine aquí a través de Google y encontré una forma bastante simple: cortar en listas. Digamos que desea poner un '&' entre todas las entradas de la lista.

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

Esto devuelve '1 y 2 y 3'.

BeckmaR
fuente
77
Acaba de volver a implementar la función de unión: `" & ".join ([str (x) para x en l])
Bryan Oakley
la concatenación de cadenas es algo ineficiente. Si len(l)=1000000en este ejemplo, el programa se ejecutará por un tiempo. appendSe recomienda afaik. l=[1,2,3]; l.append(4);
plhn
18

El 'código entre' es un ejemplo del patrón Head-Tail .

Tiene un elemento, seguido de una secuencia de pares (entre elementos). También puede ver esto como una secuencia de (elemento, entre) pares seguidos de un elemento. Generalmente es más simple tomar el primer elemento como especial y todos los demás como el caso "estándar".

Además, para evitar repetir el código, debe proporcionar una función u otro objeto que contenga el código que no desea repetir. Incrustar una declaración if en un bucle que siempre es falso, excepto que una vez es un poco tonto.

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 )

Esto es más confiable porque es un poco más fácil de probar, no crea una estructura de datos adicional (es decir, una copia de una lista) y no requiere mucha ejecución desperdiciada de una condición if que siempre es falsa, excepto una vez.

S.Lott
fuente
44
Las llamadas a funciones son mucho más lentas que las ifdeclaraciones, por lo que el argumento de "ejecución desperdiciada" no se cumple.
Ferdinand Beyer el
1
No estoy seguro de cuál es la diferencia de velocidad entre la llamada a la función y la instrucción if con algo. El punto es que esta formulación tiene declaración si-no que siempre es falsa (excepto una vez.)
S. Lott
1
Interpreté su declaración "... y no requiere mucha ejecución desperdiciada de una condición if que siempre es falsa, excepto una vez" como "... y es más rápida ya que ahorra un par de ifs". ¿Obviamente te estás refiriendo a la "limpieza del código"?
Ferdinand Beyer el
¿Definir una función en lugar de usar una ifdeclaración realmente se considera más limpia por la comunidad de Python?
Markus von Broady
17

Si simplemente está buscando modificar el último elemento data_list, simplemente puede usar la notación:

L[-1]

Sin embargo, parece que estás haciendo más que eso. No hay nada realmente malo en tu camino. Incluso eché un vistazo rápido a algún código de Django para sus etiquetas de plantilla y hacen básicamente lo que estás haciendo.

Bartek
fuente
1
No lo estoy modificando, lo estoy usando para hacer algo
E.tadeu
44
@ e.tadeu ni siquiera importa si lo estás modificando o no. Cambiar su declaración if a: if data != datalist[-1]:y mantener todo lo demás igual sería la mejor manera de codificar esto en mi opinión.
spacetyper
8
@spacetyper Esto se rompe cuando el último valor no es único.
Ark-kun
14

Si los artículos son únicos:

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

otras opciones:

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
Palza
fuente
10

Esto es similar al enfoque de Ants Aasma pero sin usar el módulo itertools. También es un iterador rezagado que anticipa un único elemento en la secuencia del iterador:

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
Andrew Dalke
fuente
4

Puede usar una ventana deslizante sobre los datos de entrada para echar un vistazo al siguiente valor y usar un centinela para detectar el último valor. Esto funciona en cualquier iterable, por lo que no necesita saber la longitud de antemano. La implementación por pares es de recetas 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
Hormigas Aasma
fuente
3

¿No hay posibilidad de iterar sobre todo, excepto el último elemento, y tratar el último fuera del bucle? Después de todo, se crea un bucle para hacer algo similar a todos los elementos sobre los que se bucle; Si un elemento necesita algo especial, no debería estar en el bucle.

(vea también esta pregunta: does-the-last-element-in-a-loop-merece-un-tratamiento-separado )

EDITAR: dado que la pregunta es más sobre el "en el medio", o bien el primer elemento es el especial porque no tiene predecesor, o el último elemento es especial porque no tiene sucesor.

xtofl
fuente
Pero el último elemento debe tratarse de manera similar a todos los demás elementos de la lista. El problema es lo que debe hacerse solo entre elementos.
e.tadeu
En ese caso, el primero es el único sin un predecesor. Desarme ese uno y repita el resto del código general de la lista.
xtofl
3

Me gusta el enfoque de @ ethan-t, pero while Truedesde mi punto de vista es peligroso.

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

Aquí, data_listes para que el último elemento sea igual en valor al primero de la lista. Se puede intercambiar con L, data_listpero en este caso resulta vacío después del bucle. while TrueTambién es posible usarlo si verifica que la lista no está vacía antes del procesamiento o si la verificación no es necesaria (¡ay!).

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')

Lo bueno es que es rápido. Lo malo: es destructible (se data_listvuelve vacío).

La solución más 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í, ya lo has propuesto!

Aliaksandr Klimovich
fuente
2

No hay nada malo en su camino, a menos que tenga 100 000 bucles y quiera guardar 100 000 declaraciones "si". En ese caso, puedes ir de esa manera:

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

Salidas:

1
2
3
3

Pero realmente, en tu caso siento que es exagerado.

En cualquier caso, probablemente tendrá más suerte con el corte:

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

#outputs
1
2
last : 3

o solo :

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

#outputs
1
2
3
last : 3

Eventualmente, una forma de KISS de hacer tus cosas, y eso funcionaría con cualquier iterable, incluidos los que no tienen __len__:

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

Ouputs:

1
2
3
3

Si siento que lo haría de esa manera, me parece simple.

e-satis
fuente
2
Pero tenga en cuenta que iterable [-1] no funcionará para todos iterables (como generador que no tienen len )
e.tadeu
Si todo lo que desea es acceder al último elemento después del ciclo, simplemente use en itemlugar de volver a calcularlo usando list[-1]. Pero sin embargo: no creo que esto sea lo que el OP estaba pidiendo, ¿verdad?
Ferdinand Beyer
Re: iterable.__iter__() - por favor no llame a las __funciones directamente. Debe ser iter(iterable).
PaulMcG
2

Use el corte y ispara verificar el último 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>

Advertencia : esto solo funciona si todos los elementos de la lista son realmente diferentes (tienen diferentes ubicaciones en la memoria). Debajo del capó, Python puede detectar elementos iguales y reutilizar los mismos objetos para ellos. Por ejemplo, para cadenas del mismo valor y enteros comunes.

Roger Dahl
fuente
2

si está revisando la lista, para mí esto también funcionó:

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

Google me trajo a esta vieja pregunta y creo que podría agregar un enfoque diferente a este problema.

La mayoría de las respuestas aquí tratarían con un tratamiento adecuado de un control de bucle for como se le preguntó, pero si data_list es destructible, sugeriría que extraiga los elementos de la lista hasta que termine con una lista vacía:

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()

incluso podría usar while len (element_list) si no necesita hacer nada con el último elemento. Encuentro esta solución más elegante que tratar con next ().

Anderson Santos
fuente
2

Para mí, la forma más simple y pitónica de manejar un caso especial al final de una lista es:

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

Por supuesto, esto también se puede usar para tratar el primer elemento de una manera especial.

Chris
fuente
2

En lugar de contar, también puedes contar:

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

Retrasar el manejo especial del último elemento hasta después del ciclo.

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

Puede haber múltiples formas. rebanar será más rápido. Agregando uno más que usa el método .index ():

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

Asumiendo la entrada como un iterador, esta es una forma de usar tee e izip de 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.

Manifestación:

>>> 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
>>>
Luego
fuente
0

La solución más simple que viene a mi mente es:

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

Por lo tanto, siempre miramos hacia adelante un elemento retrasando la iteración de procesamiento. Para omitir hacer algo durante la primera iteración, simplemente capto el error.

Por supuesto, debe pensar un poco, para NameErrorque se levante cuando lo desee.

También mantenga el `aconsejar

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

Esto se basa en que el nombre nuevo no se definió previamente. Si eres paranoico, puedes asegurarte de que newno exista usando:

try:
    del new
except NameError:
    pass

Alternativamente, también puede usar una declaración if ( if notfirst: print(new) else: notfirst = True). Pero hasta donde yo sé, la sobrecarga es mayor.


Using `timeit` yields:

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

así que espero que los gastos generales sean ilegibles.

DerWeh
fuente
0

Cuente los artículos una vez y manténgase al día con la cantidad de artículos restantes:

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

De esta manera solo evalúa la longitud de la lista una vez. Muchas de las soluciones en esta página parecen suponer que la longitud no está disponible por adelantado, pero eso no es parte de su pregunta. Si tienes la longitud, úsala.

Ethan T
fuente
0

Una solución simple que viene a la mente sería:

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

Se puede lograr una segunda solución igualmente simple utilizando un contador:

# 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
Lo que sea esta bien
fuente