Pull to refresh

Comments 43

Кажутся ли вам циклы, построенные с использованием enumerate(), более читабельными, чем циклы, созданные с использованием range(len())?

Они не эквивалентны в случае, если итерируемый список изменяет длину в процессе итерации поскольку диапазон range фиксируется по длине в начале цикла и потом не меняется.

Менять набор данных на ходу во время забега по нему — это экстремальное развлечение.
Оно хорошо, когда надо оптимизировать производительность, а во всех остальных случаях — плохо.

Набор данных может не то, что меняться, а, например, генериться/читаться из источника данных на ходу. Это совершенно нормально, например, при чтении больших файлов. Зачем читать сразу весь файл в память, если его можно обрабатывать построчно?

Для таких случаев есть генераторы. Они как раз для описанных выше случаев.

Ну да, а потребляется созданный генераторами контент как-раз через такой for, например. И при желании enumerate.

Я имею в виду вот что:


for item in the_list:
    ....
    the_list += more_data
    ....
    del the_list[from:to:step]
    ....

Вот это — экстремальное развлечение. Потому что зависит от устройства итератора по списку.


for i in range(len(the_list)):
    item = the_list[i]
    ....
    the_list += more_data
    ....
    del the_list[from:to:step]
    ....

Более предсказуемо (всё на виду), но требует офигенной тщательности и понимания, как изменяется структура в процессе забега.


А если у вас есть отдельно источник, отдельно приёмник — то это ок


for item in the_source:
    ....
    the_receiver.append(processed(item))
    ....
Удалять что-то из итерируемого контейнера, который не предназначен для того, чтобы из него что-то в процессе итерации удалять — это всегда плохая идея. Есть же очередь для этого, например, или стэк. Забираешь из них элементы по одному — и делай с ними что хочешь. Они специально для таких случаев и придуманы.

Но это не "итерируемый список меняет длину".


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


А главное, — зачем всё это нужно? Одновременно итерироваться по очереди и изменять её?
И нельзя ли выразить то же самое какими-то более прозрачными и очевидными средствами? (циклом while, в конце концов)

На самом деле, даже в JavaScript существуют методы, позволяющие перебирать массивы, так сказать, без посредников. Речь идёт о циклах forEach и for of.

Так в питоне если сделаем так, тоже получается без посредников:
for x in scores:..

И с enumerate() все классно, не думаю, что у кого то к нему претензии, но range() уж точно не нужно так использовать.
Просто с
for x in scores:..
нельзя обратиться к индексу элемента, а это иногда нужно.
это да, без сомнений.
Но автор говорит «даже в JavaScript существуют методы, позволяющие перебирать массивы, так сказать, без посредников» и при этом не приводит в пример данный способ, который является «без посредников». Да и в JS, чтобы обратиться к индексу нужно прибегнуть к помощи «посредников», по крайней мере обычного массива, на сколько я знаю
Когда нужно обратиться по индексу, такие циклы и не используют. Циклы формата foreach удобны для потоковых и прочих данных с последовательным доступом.
Какие же например в пайтоне использовать циклы для получения индекса?
Какие же например в пайтоне использовать циклы для получения индекса?
А зачем вообще другой цикл использовать, если благодаря наличию универсальных механизмов enumerate, range, zip, slice и т. д., можно гибче, и при этом не сложнее, получить больше комбинаций применения?

Речь о том, что тот же zip или slice можно использовать не только в циклах. Так зачем запоминать семантику двух разных подходов (for и foreach), когда и циклы, и остальное, можно положить поверх одного for, применяя вспомогательные простые универсальные механизмы, которые касаются не только циклов?
Зато очень читаемые конструкции. Особенно, если использовать человекочитаемые имена.
person_names = ["John", "Mike", "Ashurbanipal"]
for person_name in person_names:
    do_something(person_name)
Вот да, а когда начинают шаманить с индексами, бывает фиг поймёшь, чего автор кода вообще хотел. Если и надо доступ сразу к нескольким элементам последовательности, то лучше уж через zip и т.п.
С enumerate хорошо ещё то, что это итератор и на вход он принимает тоже итератор. То есть ему можно передать, например, открытый файл или генератор и он с ним будет прекрасно работать элемент за элементом. И это правильный подход, ведь при каждом шаге цикла нас интересует только текущий элемент и его номер, а знание о том, какой длины будет последовательность, переданная на итерирование в цикл — оно совершенно лишнее.
Такой подход характерен не только для питона. В некоторых языках вообще циклов нет.
имея опыт работы с другим популярным языком программирования, вроде PHP или JavaScript, то вам знакома методика применения переменной-счётчика, хранящей, например, индекс текущего элемента массива, обрабатываемого в цикле.

В PHP самым стандартным является такой подход:


$scores = [54, 67, 48, 99, 27];
foreach ($scores as $k => $score) {
    echo "{$k} {$score}\n";
}

Банальнейшая вещь и с незапамятных времён.
Но это, конечно же, не совсем enumerate, ведь вместо инкремента тут ключи ассоциативного массива (в данном случае не отличающиеся).


А вот сие уже гарантированно эквивалентно:


$scores = [54, 67, 48, 99, 27];
foreach (array_values($scores) as $i => $score) {
    echo ($i + 1) . " {$score}\n";
}

Сам enumerate тоже можно переизобрести, если очень хочется:


function enumerate(array $array, int $start = 0): array
{
    $keys = range($start, count($array) - 1 + $start);
    return array_combine($keys, $array);
}

$scores = [54, 67, 48, 99, 27];
foreach (enumerate($scores, 1) as $i => $score) {
    echo "{$i} {$score}\n";
}

Либо с yield (медленнее при использовании с массивами (PHP 7.4)):


function enumerate(iterable $iterable, int $start = 0): iterable
{
    $i = $start;
    foreach ($iterable as $v) {
        yield $i++ => $v;
    }
}
Подскажите, кто и как итерируется по двумерным массивам? Хотелось бы 1) наглядно 2) кратко
а есть какой нибудь вариант в одну строку типа
for x,y, value in array:

Есть такой длинный и уродливый:


for x, y, value in [(x, y, v) for (x, l) in enumerate(array) for (y, v) in enumerate(l)]:

Лучше уж в две строки.

Не чистый Python, но есть такой вариант в numpy:


import numpy as np

array = [[1, 2], [3, 4]]
for (i, j), item in np.ndenumerate(array):
    print(f"array[{i}, {j}] = {item}")

array[0, 0] = 1
array[0, 1] = 2
array[1, 0] = 3
array[1, 1] = 4

Также работает с размерностями > 2.

Что тут «питонистического»? Очень кривое описание одной функции стандартной библиотеки.
«Питонистичность» — это когда не плодятся сущности сверх необходимого. Например, список индексов при итерировании списка в питоне в общем-то не нужен.
scores = [54,67,48,99,27]
for i in range(len(scores)):
   print(i, scores[i])

Проблема этого цикла заключается в том, что он не очень хорошо соответствует идеологии Python. В нём мы не перебираем список, а, вместо этого, используем вспомогательную переменную i для обращения к элементам списка.


Я так и не понял в чем, собственно, проблема? Что если мне нужно организовать цикл на заданное количество шагов, не работая непосредственно со списком? Сколько раз читал про это, но нигде не увидел реально обоснованой причины так не делать. Заявления, что это «непитонично», звучат как пук в банку. Могли бы хоть сравнения замеров быстродействия выложить или детальный разбор механизмов работы.
Но в данном примере вы таки работаете со списком — сначала запрашиваете у него размер, а потом ещё и запрашиваете элементы по индексу. Если сам список вам не нужен, а нужен только его размер, то претензия понятна. В противном случае получается, что вы делаете лишние запросы к списку и заводите лишнюю переменную. А ведь без этого можно было обойтись.
Оно всё так, но…
«Питонистический подход к циклам for: range() и enumerate()»

Автор заметки, перевод которой мы сегодня публикуем, хочет рассказать о некоторых особенностях использования циклов for в Python.


В самой статье же рассматривается только итерация по спискам, хотя это вообще не является прямой задачей цикла «for». Так что тут либо название статьи менять, либо раскрывать тему нормально, а то сейчас впечатлительные начитаются и будут везде писать что «for i in range(len(seq)):» порочно независимо от контекста.

ЗЫ: Не могу влепить статье дизлайк, буду благодарен, если кто-то сделает это за меня.
И что же по вашему является «прямой задачей цикла for», если не итерация по элементам последовательности (в частности и списка тоже)?
Думаю, раз автор взял эту тему, можно было бы подробнее все раскрыть. Рассмотреть все виды циклов в питоне, пробежаться по другим структурам данных, например по словарям, а так же показать как работать с генераторами через циклы и лямбда выражения
«for» — цикл с известным количеством итераций. Не больше и не меньше. В этом его назначение и смысл, всё остальное зависит от контекста его применения. Для итерации по последовательностям применяется «foreach» и его аналоги. Это разные циклы, с разной идеологией применения, и то что в питоне их обьединили в одно не ставит между ними знак равенства.
Автор статьи же затрагивает только «foreach» составляющую, что прямо противоречит названию и аннотации статьи и может ввести людей в заблуждение.
и то что в питоне их обьединили в одно не ставит между ними знак равенства

Это не совсем так, for в Python только один, и для инкремента числа используется тот же самый обход последовательности, генерируемой функцией range.
Вот где действительно объединены, так это в C++, Java, JavaScript и (частично) в Perl5.

Строго говоря в питоне вообще нет «for» — есть только «foreach». И инкремента индекса (как и самого индекса) тоже нет — есть проход по списку, сгенерированому функцией range() или поданому явно.

С++ обычно не рассматривается в отрыве от С, так как является его надмножеством, а в С вполне есть старый добрый «for» без всяких последовательностей. В JS эти два цикла вообще вроде бы разделены друг от друга?
Строго говоря в питоне вообще нет «for» — есть только «foreach». И инкремента индекса (как и самого индекса) тоже нет — есть проход по списку, сгенерированому функцией range() или поданому явно.

Что я и сказал, да.


Строго говоря в питоне вообще нет «for» — есть только «foreach».

Касательно терминологии, есть множество языков, где for выступает именно как оператор обхода по итератору (условно).
Самый яркий пример – Raku, потому что там есть for @scores.kv -> $i, $score { и есть loop (my $i = 0; $i < @scores.elems; ++$i) {.


С++ обычно не рассматривается в отрыве от С, так как является его надмножеством, а в С вполне есть старый добрый «for» без всяких последовательностей.

Почему же не рассматривается? Рассматривается.
Особенно когда обсуждаются специфичные для C++ особенности.


Вот где действительно объединены, так это в C++, <…>

Я имею в виду, что оператор for используется для обоих вариантов цикла в C++. В C, естественно – нет.

Я лишь хотел сказать, что что в питоне забавная версия обьединения двух разных по сути циклов с названием одного, а механикой другого. И говоря, что конструкцию вида «for i in range(len(seq)):» не стоит использовать, нужно уточнять, что это касается искличительно ситуаций итерирования обьекта. Для имитации же именно поведения «for» эта конструкция вполне легальна.
И говоря, что конструкцию вида «for i in range(len(seq)):» не стоит использовать, нужно уточнять, что это касается исключительно ситуаций итерирования обьекта. Для имитации же именно поведения for эта конструкция вполне легальна.

Я не спорю, и статья явно направлена на тех, кто совсем не в курсе и допускает довольно серьёзные неточности.


с названием одного, а механикой другого.

Моя же позиция в том, что название for часто используется и для другой механики во многих языках.
Например, в Rust, Kotlin, Julia, Nim, Zig.

«то что в питоне их обьединили в одно не ставит между ними знак равенства», но данный пост ведь про Python, разве нет? :)
Кстати, и в других языках в цикле for не обязательно заранее знать число итераций, условие выхода из цикла может быть довольно произвольным. Так что это совсем не критерий. Вот итерирование по элементам против выхода по условию — это реальная разница между foreach и for.
В питоне решили, что выход из цикла по условию недостаточно красив для этого языка и не стоит специальной конструкции. Хотя если хочется из цикла всегда можно выйти изнутри цикла по условию.
Уважаемый, просто найдите ближайшего человека, обучающего людей програмированию (программированию, а не навыкам владения конкретным языком) и спросите чем отличаются циклы «for», «foreach» и «while», и он вам все доступно обьяснит.
Что касается цикла с выходом по условию, то в питоне есть нужный инструмент и зовется он «while». И он достаточно красив для этого языка.
Ну вообще-то в разных ЯП разные типовые алгоритмические конструкции могут реализовываться различным способом, с помощью разных ключевых слов и конструкций. Так то я с вами согласен, что питоновский for — это по сути foreach в других языках. Но это вопрос наименования сущностей. В питоне вот так назвали (видимо, для краткости) и придётся это как-то терпеть теперь. :)
К оригинальной статье есть комментарий про быстродействие. for показал немногим лучшие результаты.
list comprehensions и распаковка переменных наше все!
test_list = [1,2,3,4,5,6]
[print(*ll) for ll in enumerate(list)]
А сейчас вы создали список из нанов и выкинули его. Не надо юзать компрехеншены там, где они вообще не в тему.
Sign up to leave a comment.