Как питонный способ обнаружить последний элемент в цикле for?


188

Я хотел бы знать лучший способ (более компактный и "питонический" способ) сделать специальную обработку для последнего элемента в цикле for. Существует фрагмент кода, который следует вызывать только между элементами, подавляя в последнем.

Вот как я сейчас это делаю:

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

Есть ли лучший способ?

Примечание: я не хочу делать это с помощью таких хаков, как использование reduce.;)


А как насчет первого? Должно ли это быть подавлено тоже?
Адам Матан

не могли бы вы сказать нам, что делается между элементами?
SilentGhost

2
Я хотел бы получить ответ для общего случая, но конкретный случай, когда мне это нужно, - это запись вещей в потоке с разделителями между ними, как stream.write (',' .join (name_list)), но делать это в цикле for без объединения строк, потому что там много
записей


Первые три строки этого ответа действительно помогли мне, у меня была похожая проблема.
кардамон

Ответы:


151

В большинстве случаев проще (и дешевле) сделать первую итерацию особой, а не последнюю:

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

    item()

Это будет работать для любого повторяемого, даже для тех, которые не имеют len():

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

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

Кроме того, я не думаю, что есть вообще превосходное решение, поскольку оно зависит от того, что вы пытаетесь сделать. Например, если вы строите строку из списка, естественно, ее лучше использовать, str.join()чем использовать forцикл «с особым случаем».


Используя тот же принцип, но более компактный:

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

Выглядит знакомо, не правда ли? :)


Для @ofko и других, которым действительно необходимо выяснить, является ли текущее значение итерируемого без len()последнего, вам нужно смотреть в будущее:

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

Тогда вы можете использовать это так:

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

1
Правда, этот способ кажется лучше моего, по крайней мере, для него не нужно использовать enumerate и len.
e.tadeu

Да, но он добавляет еще один, ifкоторого можно было бы избежать, если бы цикл был разбит на два цикла. Однако это актуально только при итерации огромного списка данных.
Адам Матан

Проблема разделения на два цикла заключается в том, что он либо нарушает DRY, либо заставляет вас определять методы.
e.tadeu

Я действительно пытаюсь понять ваш последний пример (который работает безупречно в моем коде), но я не понимаю, как он работает (идея позади)
Оливье Понс

1
@OlivierPons Вам нужно понять протокол итератора Python: я получаю итератор для объекта и получаю первое значение с помощью next(). Затем я использую тот факт, что итератор сам по себе итеративен, поэтому я могу использовать его в forцикле до исчерпания, итерации от второго до последнего значения. В течение этого времени я сохраняю текущее значение, полученное от итератора, и yieldпоследнее. Таким образом, я знаю, что есть еще одна ценность. После цикла for я сообщал о каждом значении, кроме последнего.
Фердинанд Бейер

20

Хотя этот вопрос довольно старый, я пришел сюда через Google и нашел довольно простой способ: нарезка списка. Допустим, вы хотите поставить '&' между всеми записями списка.

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

Это возвращает «1 & 2 & 3».


7
Вы только что переопределили функцию соединения: `" & ".join ([str (x) для x in l])
Брайан Оукли

конкатенация строк несколько неэффективна. Если len(l)=1000000в этом примере программа будет работать некоторое время. appendрекомендуется афаик. l=[1,2,3]; l.append(4);
plhn

18

«Код между» является примером шаблона « голова-хвост» .

У вас есть предмет, за которым следует последовательность пар (между предметами). Вы также можете просмотреть это как последовательность пар (элемент, между), за которыми следует элемент. Обычно проще воспринимать первый элемент как особый, а все остальные - как «стандартный» случай.

Кроме того, чтобы избежать повторения кода, вы должны предоставить функцию или другой объект, содержащий код, который вы не хотите повторять. Внедрение оператора if в цикл, который всегда ложен, за исключением одного раза, немного глупо.

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 )

Это более надежно, потому что его немного легче доказать. Он не создает дополнительной структуры данных (то есть копии списка) и не требует много потерянного выполнения условия if, которое всегда ложно, за исключением одного раза.


4
Вызовы функций намного медленнее, чем ifоператоры, поэтому аргумент «потраченное впустую исполнение» не выполняется.
Фердинанд Бейер

1
Я не уверен, какая разница в скорости между вызовом функции и оператором if имеет отношение к чему-либо. Дело в том, что в этой формулировке нет оператора if, который всегда ложен (кроме одного раза)
S.Lott

1
Я интерпретировал ваше утверждение «… и не требует много напрасного выполнения условия if, которое всегда ложно, кроме одного раза», как «… и быстрее, так как экономит пару ifсекунд». Очевидно, вы просто ссылаетесь на «чистоту кода»?
Фердинанд Бейер

Определяет функцию вместо использования if утверждения действительно более чистым в сообществе Python?
Маркус фон

17

Если вы просто хотите изменить последний элемент, data_listвы можете просто использовать обозначение:

L[-1]

Тем не менее, похоже, что вы делаете больше, чем это. В твоем пути нет ничего плохого. Я даже бросил быстрый взгляд на некоторый код Django для их тегов шаблонов, и они в основном делают то, что вы делаете.


1
Я не изменяю это, я использую это, чтобы сделать что-то
e.tadeu

4
@ e.tadeu, даже не важно, изменишь ты это или нет. На if data != datalist[-1]:мой взгляд, изменение выражения if на: и сохранение всего остального было бы наилучшим способом кодирования.
spacetyper

8
@spacetyper Это прерывается, когда последнее значение не является уникальным.
Арк-кун

14

если предметы уникальны:

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

другие варианты:

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

Это похоже на подход Антса Аасмы, но без использования модуля itertools. Это также запаздывающий итератор, который просматривает один элемент в потоке итератора:

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

Вы можете использовать скользящее окно поверх входных данных, чтобы просмотреть следующее значение и использовать часовой для определения последнего значения. Это работает для любой итерации, поэтому вам не нужно заранее знать длину. Парная реализация взята из рецептов 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

Нет ли возможности перебрать все элементы, кроме последнего, и обработать последний вне цикла? В конце концов, создается цикл, который делает что-то похожее на все элементы, над которыми вы зацикливаетесь; если одному элементу нужно что-то особенное, оно не должно быть в цикле.

(см. также этот вопрос: « делает ли последний элемент в цикле достойный отдельного обращения» )

РЕДАКТИРОВАТЬ: так как вопрос больше о «между», либо первый элемент является особенным в том, что он не имеет предшественника, или последний элемент является особенным в том, что он не имеет преемника.


Но последний элемент должен обрабатываться аналогично любому другому элементу в списке. Проблема в том, что нужно делать только между элементами.
e.tadeu

В этом случае первый является единственным без предшественника. Разберите его на части и переберите остальную часть общего списка кода.
xtofl

3

Мне нравится подход @ ethan-t, но while Trueон опасен с моей точки зрения.

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

Здесь data_listтак, что последний элемент по значению равен первому из списка. L может быть заменен на, data_listно в этом случае он будет пустым после цикла. while Trueтакже возможно использовать, если вы проверяете, что список не пуст перед обработкой или проверка не нужна (ой!).

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

Хорошая часть в том, что это быстро. Плохое - это разрушаемо (data_list становится пустым).

Наиболее интуитивное решение:

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

Ах, да, вы уже предложили это!


2

В вашем пути нет ничего плохого, если у вас не будет 100 000 циклов и вы хотите сохранить 100 000 операторов if. В этом случае вы можете пойти по этому пути:

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

Выходы:

1
2
3
3

Но на самом деле, в вашем случае я чувствую, что это излишне.

В любом случае вам наверняка повезет с нарезкой:

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

#outputs
1
2
last : 3

или просто :

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

#outputs
1
2
3
last : 3

В конце концов, KISS способ сделать что-то, и это будет работать с любой итерацией, в том числе без __len__:

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

Выходы:

1
2
3
3

Если мне кажется, что я бы так поступил, мне кажется, это просто.


2
Но обратите внимание, что итерируемый [-1] не будет работать для всех итераций (таких как генератор, у которого нет len )
e.tadeu

Если все, что вам нужно, это получить доступ к последнему элементу после цикла, просто используйте itemвместо пересчета его с помощью list[-1]. Но тем не менее: я не думаю, что это то, о чем просил ФП, не так ли?
Фердинанд Бейер

Re: iterable.__iter__() - пожалуйста, не вызывайте __функции напрямую. Должно быть iter(iterable).
PaulMcG

2

Используйте нарезку и, isчтобы проверить последний элемент:

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 : Это работает только в том случае, если все элементы в списке на самом деле разные (имеют разные места в памяти). Под капотом Python может обнаружить одинаковые элементы и использовать для них одни и те же объекты. Например, для строк с одинаковым значением и общими целыми числами.


2

если вы просматриваете список, для меня это тоже сработало:

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

2

Google привел меня к этому старому вопросу, и я думаю, что мог бы добавить другой подход к этой проблеме.

Большинство ответов здесь будут касаться правильной обработки элемента управления цикла for, как его просили, но если data_list разрушаем, я бы посоветовал вам выталкивать элементы из списка до тех пор, пока вы не получите пустой список:

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

вы можете даже использовать while len (element_list), если вам не нужно ничего делать с последним элементом. Я считаю это решение более элегантным, чем работа с next ().


2

Для меня самый простой и питонный способ обработки особого случая в конце списка:

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

Конечно, это также может быть использовано для особой обработки первого элемента.


2

Вместо того, чтобы считать, вы также можете считать:

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

1

Задержка специальной обработки последнего элемента до окончания цикла.

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

1

Там может быть несколько способов. нарезка будет самой быстрой. Добавляем еще один, который использует метод .index ():

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

0

Предполагая ввод в качестве итератора, вот способ использования tee и izip из 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.

Демо-версия:

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

Самое простое решение, которое приходит мне в голову:

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

Поэтому мы всегда смотрим в будущее на один пункт, задерживая выполнение одной итерации. Чтобы пропустить что-то во время первой итерации, я просто улавливаю ошибку.

Конечно, вам нужно немного подумать, NameErrorчтобы поднять, когда вы этого хотите.

Также держите `Сундук`

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

Это полагает, что имя new ранее не было определено. Если вы параноик, вы можете убедиться, что newего не существует, используя:

try:
    del new
except NameError:
    pass

В качестве альтернативы вы также можете использовать оператор if ( if notfirst: print(new) else: notfirst = True). Но, насколько я знаю, накладные расходы больше.


Using `timeit` yields:

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

поэтому я ожидаю, что накладные расходы не будут выбраны.


0

Подсчитайте предметы один раз и следите за количеством оставшихся предметов:

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

Таким образом, вы оцените длину списка только один раз. Многие из решений на этой странице, кажется, предполагают, что длина заранее недоступна, но это не часть вашего вопроса. Если у вас есть длина, используйте ее.


0

На ум приходит одно простое решение:

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

Второе столь же простое решение может быть достигнуто с помощью счетчика:

# 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
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.