Jaki jest pythonowy sposób wykrywania ostatniego elementu w pętli „for”?

188

Chciałbym znać najlepszy sposób (bardziej zwarty i „pytoniczny”), aby wykonać specjalne traktowanie ostatniego elementu w pętli for. Istnieje fragment kodu, który powinien być wywoływany tylko między elementami, w ostatnim jest tłumiony.

Oto jak obecnie to robię:

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

Czy jest jakiś lepszy sposób?

Uwaga: nie chcę tego robić przy użyciu takich hacków, jak używanie reduce.;)

e.tadeu
źródło
A co z pierwszym? Czy to też powinno zostać stłumione?
Adam Matan
czy możesz nam powiedzieć, co się dzieje między elementami?
SilentGhost
2
Chciałbym uzyskać odpowiedź na ogólny przypadek, ale konkretnym przypadkiem, w którym potrzebuję tego, jest pisanie rzeczy w strumieniu, z separatorami między nimi, podobnie jak stream.write (',' .join (name_list)), ale robi to w pętli for bez łączenia łańcuchów, ponieważ jest wiele zapisów ...
e.tadeu
Pierwsze trzy linijki tej odpowiedzi naprawdę mi pomogły, miałem podobne wyzwanie.
kardamon

Odpowiedzi:

151

W większości przypadków łatwiej (i taniej) jest zrobić pierwszą iterację specjalnym przypadkiem zamiast ostatniego:

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

    item()

Będzie to działać dla każdego iterowalnego, nawet dla tych, które nie mają len():

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

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

Poza tym nie sądzę, że istnieje ogólnie lepsze rozwiązanie, ponieważ zależy to od tego, co próbujesz zrobić. Na przykład, jeśli budujesz ciąg z listy, naturalnie lepiej jest użyć str.join()niż forpętli „ze specjalnym przypadkiem”.


Stosując tę ​​samą zasadę, ale bardziej kompaktową:

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

Wygląda znajomo, prawda? :)


W przypadku @ofko i innych, którzy naprawdę muszą dowiedzieć się, czy aktualna wartość iterowalności bez len()jest ostatnia, musisz spojrzeć w przyszłość:

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

Następnie możesz użyć tego w następujący sposób:

>>> for i, has_more in lookahead(range(3)):
...     print(i, has_more)
0 True
1 True
2 False
Ferdinand Beyer
źródło
1
To prawda, że ​​ten sposób wydaje się lepszy niż mój, przynajmniej nie trzeba używać wyliczenia i len.
e.tadeu
Tak, ale dodaje kolejną, ifktórej można by uniknąć, gdyby pętla została podzielona na dwie pętle. Jest to jednak istotne tylko przy iteracji ogromnej listy danych.
Adam Matan
Problem z podziałem na dwie pętle polega na tym, że albo narusza DRY, albo zmusza do zdefiniowania metod.
e.tadeu
Naprawdę staram się zrozumieć twój ostatni przykład (który działa bezbłędnie w moim kodzie), ale nie rozumiem, jak to działa (pomysł za nim)
Olivier Pons
1
@OlivierPons Musisz zrozumieć protokół iteratora Pythona: Dostaję iterator dla obiektu i za pomocą pierwszej pobieram pierwszą wartość next(). Następnie wykorzystuję to, że iterator sam jest iterowalny, więc mogę go używać w forpętli do wyczerpania, iteracji od drugiej do ostatniej wartości. Podczas tego procesu zachowuję lokalnie wartość pobraną z iteratora lokalnie i yieldostatnią zamiast tego. W ten sposób wiem, że będzie jeszcze jedna wartość. Po pętli for zgłosiłem każdą wartość oprócz ostatniej.
Ferdinand Beyer,
20

Chociaż to pytanie jest dość stare, przyszedłem tutaj przez Google i znalazłem dość prosty sposób: cięcie list. Powiedzmy, że chcesz wstawić znak „&” między wszystkimi wpisami listy.

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

Zwraca „1 i 2 i 3”.

BeckmaR
źródło
7
Właśnie zaimplementowałeś funkcję łączenia: `` & '.join ([str (x) for x in l])
Bryan Oakley
konkatenacja łańcuchów jest nieco nieefektywna. Jeśli len(l)=1000000w tym przykładzie program będzie działał przez chwilę. appendjest zalecane afaik. l=[1,2,3]; l.append(4);
plnn
18

„Kod między” jest przykładem wzoru głowa-ogon .

Masz przedmiot, po którym następuje sekwencja par (pomiędzy, przedmiot). Możesz także zobaczyć to jako sekwencję par (element, między), a następnie element. Zasadniczo łatwiej jest potraktować pierwszy element jako wyjątkowy, a wszystkie pozostałe jako „standardową” obudowę.

Ponadto, aby uniknąć powtarzania kodu, musisz podać funkcję lub inny obiekt zawierający kod, którego nie chcesz powtarzać. Osadzanie instrukcji if w pętli, która zawsze jest fałszywa, z wyjątkiem jednego razu, jest trochę głupie.

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 )

Jest to bardziej niezawodne, ponieważ nieco łatwiej jest udowodnić, że nie tworzy dodatkowej struktury danych (tj. Kopii listy) i nie wymaga dużo zmarnowanego wykonania warunku if , który zawsze jest fałszywy, z wyjątkiem jednego.

S.Lott
źródło
4
Wywołania funkcji są znacznie wolniejsze niż ifinstrukcje, więc argument „zmarnowane wykonanie” nie ma zastosowania.
Ferdinand Beyer
1
Nie jestem pewien, co różnica prędkości między wywołaniem funkcji a instrukcją if ma cokolwiek wspólnego. Chodzi o to, że w tym sformułowaniu nie ma wyrażenia if, które zawsze jest fałszywe (z wyjątkiem jednego).
S.Lott,
1
Zinterpretowałem twoje stwierdzenie „… i nie wymaga dużo zmarnowanego wykonania warunku if, który zawsze jest fałszywy, z wyjątkiem jednego”, jako „… i jest szybszy, ponieważ oszczędza kilka ifs”. Oczywiście masz na myśli „czystość kodu”?
Ferdinand Beyer
Czy zdefiniowanie funkcji zamiast używania ifinstrukcji jest naprawdę uważane za czystsze przez społeczność Python?
Markus von Broady,
17

Jeśli chcesz po prostu zmodyfikować ostatni element data_list, możesz po prostu użyć notacji:

L[-1]

Wygląda jednak na to, że robisz więcej. Nie ma nic złego na twojej drodze. Rzuciłem nawet okiem na kod Django dla ich znaczników szablonów i robią to, co robisz.

Bartek
źródło
1
Nie modyfikuję tego, używam go do zrobienia czegoś
e.tadeu
4
@ e.tadeu nie ma znaczenia, czy go modyfikujesz, czy nie. Zmiana zdania if na: if data != datalist[-1]:i utrzymanie tego samego wszystkiego byłoby moim zdaniem najlepszym sposobem na zakodowanie tego.
spacetyper
8
@spacetyper Dzieje się tak, gdy ostatnia wartość nie jest unikalna.
Ark-kun
14

jeśli przedmioty są unikalne:

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

inne opcje:

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
źródło
10

Jest to podobne do podejścia Ants Aasma, ale bez użycia modułu itertools. Jest to również opóźniony iterator, który wyprzedza pojedynczy element w strumieniu iteratora:

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
źródło
4

Możesz użyć przesuwanego okna nad danymi wejściowymi, aby uzyskać podgląd następnej wartości i użyć wartownika do wykrycia ostatniej wartości. Działa to na każdym iterowalnym, więc nie musisz wcześniej znać długości. Implementacja parami pochodzi z receptur 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
Mrówka osocza
źródło
3

Czy nie ma możliwości iteracji nad wszystkim oprócz ostatniego elementu i potraktowania ostatniego poza pętlą? W końcu tworzona jest pętla, aby zrobić coś podobnego do wszystkich elementów, nad którymi zapętlasz; jeśli jeden element potrzebuje czegoś specjalnego, nie powinien znajdować się w pętli.

(patrz także to pytanie: czy ostatni element w pętli zasługuje na osobne traktowanie )

EDYCJA: ponieważ pytanie dotyczy bardziej „pomiędzy”, albo pierwszy element jest szczególny, ponieważ nie ma poprzednika, lub ostatni element jest wyjątkowy, ponieważ nie ma następcy.

xtofl
źródło
Ale ostatni element powinien być traktowany podobnie jak każdy inny element na liście. Problem polega na tym, że należy to robić tylko między elementami.
e.tadeu
W takim przypadku pierwszy jest jedynym bez poprzednika. Rozłóż to na części i przewiń resztę kodu ogólnego listy.
xtofl
3

Lubię podejście @ ethan-t, ale while Truejest niebezpieczne z mojego punktu widzenia.

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

Tutaj data_listjest tak, że ostatni element jest równy pod względem wartości do pierwszego z listy. L może być zamienione, data_listale w tym przypadku po pętli jest puste. while Truemożna również użyć, jeśli sprawdzisz, czy lista nie jest pusta przed przetwarzaniem lub sprawdzenie nie jest potrzebne (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')

Najlepsze jest to, że jest szybki. Złe - jest zniszczalne ( data_liststaje się puste).

Najbardziej intuicyjne rozwiązanie:

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

O tak, już to zaproponowałeś!

Aliaksandr Klimovich
źródło
2

Na twojej drodze nie ma nic złego, chyba że będziesz mieć 100 000 pętli i chcesz zaoszczędzić 100 000 instrukcji „jeśli”. W takim przypadku możesz przejść w ten sposób:

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

Wyjścia:

1
2
3
3

Ale tak naprawdę, w twoim przypadku wydaje mi się, że to przesada.

W każdym razie prawdopodobnie będziesz miał więcej szczęścia z krojeniem:

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

#outputs
1
2
last : 3

Lub tylko :

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

#outputs
1
2
3
last : 3

W końcu KISS to sposób na zrobienie czegoś, co działałoby z każdym iterowalnym, w tym bez __len__:

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

Ouputs:

1
2
3
3

Jeśli wydaje mi się, że zrobiłbym to w ten sposób, wydaje mi się to proste.

e-satis
źródło
2
Ale zauważ, że iterowalny [-1] nie będzie działał dla wszystkich iteracyjnych (takich jak generator, który nie ma len )
e.tadeu
Jeśli wszystko, czego chcesz, to dostęp do ostatniego elementu po pętli, po prostu użyj itemzamiast go ponownie obliczać za pomocą list[-1]. Niemniej jednak: nie sądzę, aby o to prosił PO, prawda?
Ferdinand Beyer
Re: iterable.__iter__() - proszę nie wywoływać __funkcji bezpośrednio. Powinno być iter(iterable).
PaulMcG
2

Użyj krojenia i, isaby sprawdzić ostatni element:

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>

Zastrzeżenie emptor : Działa to tylko wtedy, gdy wszystkie elementy na liście są faktycznie różne (mają różne lokalizacje w pamięci). Pod maską Python może wykrywać równe elementy i ponownie wykorzystywać dla nich te same obiekty. Na przykład dla ciągów o tej samej wartości i wspólnych liczbach całkowitych.

Roger Dahl
źródło
2

jeśli przeglądasz listę, dla mnie to też działało:

for j in range(0, len(Array)):
    if len(Array) - j > 1:
        notLast()
tsf144
źródło
2

Google przyniosło mi to stare pytanie i myślę, że mógłbym dodać inne podejście do tego problemu.

Większość odpowiedzi tutaj dotyczyłaby właściwego traktowania kontroli pętli for, tak jak ją poproszono, ale jeśli lista_danych jest możliwa do zniszczenia, sugerowałbym, żebyś wyskakiwał elementy z listy, aż skończy się pusta lista:

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

możesz nawet użyć while len (element_list), jeśli nie musisz nic robić z ostatnim elementem. Uważam to rozwiązanie za bardziej eleganckie niż radzenie sobie z next ().

Anderson Santos
źródło
2

Dla mnie najprostszym i pytonowym sposobem obsługi specjalnego przypadku na końcu listy jest:

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

Oczywiście można to również wykorzystać do specjalnego traktowania pierwszego elementu.

Chris
źródło
2

Zamiast liczyć, możesz także odliczać:

  nrToProcess = len(list)
  for s in list:
    s.doStuff()
    nrToProcess -= 1
    if nrToProcess==0:  # this is the last one
      s.doSpecialStuff()
jeroent
źródło
1

Opóźnij specjalną obsługę ostatniego elementu, aż do zakończenia pętli.

>>> for i in (1, 2, 3):
...     pass
...
>>> i
3
użytkownik240515
źródło
1

Może być wiele sposobów. krojenie będzie najszybsze. Dodanie jeszcze jednego, który używa metody .index ():

>>> l1 = [1,5,2,3,5,1,7,43]                                                 
>>> [i for i in l1 if l1.index(i)+1==len(l1)]                               
[43]
amolpachpute
źródło
0

Przyjmując dane wejściowe jako iterator, oto sposób użycia tee i izip z 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.

Próbny:

>>> 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
>>>
Zaraz
źródło
0

Najprostsze rozwiązanie, jakie przychodzi mi na myśl, to:

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

Dlatego zawsze patrzymy w przyszłość o jeden element, opóźniając przetwarzanie o jedną iterację. Aby pominąć robienie czegoś podczas pierwszej iteracji, po prostu wychwytuję błąd.

Oczywiście musisz się trochę zastanowić, NameErroraby zostać podniesionym, kiedy chcesz.

Zachowaj także `counstruct

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

Oznacza to, że nazwa new nie była wcześniej zdefiniowana. Jeśli jesteś paranoikiem, możesz upewnić się, że newnie istnieje, używając:

try:
    del new
except NameError:
    pass

Alternatywnie możesz oczywiście użyć instrukcji if ( if notfirst: print(new) else: notfirst = True). Ale o ile wiem, koszty ogólne są większe.


Using `timeit` yields:

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

więc spodziewam się, że koszty ogólne nie będą możliwe do wybrania.

DerWeh
źródło
0

Policz elementy raz i nadążaj za liczbą pozostałych elementów:

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

W ten sposób oceniasz długość listy tylko raz. Wiele rozwiązań na tej stronie wydaje się zakładać, że długość jest niedostępna z góry, ale nie jest to częścią twojego pytania. Jeśli masz długość, użyj jej.

Ethan T.
źródło
0

Jednym prostym rozwiązaniem, które przychodzi na myśl, byłoby:

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

Drugie równie proste rozwiązanie można uzyskać za pomocą licznika:

# 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
AnythingIsFine
źródło