На этом шаге мы дадим несколько советов по написанию циклов в Python.
Один из самых легких способов отличить разработчика с опытом работы на C-подобных языках, который совсем недавно перешел на Python, - посмотреть, как он пишет циклы.
Например, всякий раз, когда встречается следующий фрагмент кода, который выглядит, как показано ниже, то сразу становится понятным, что тут пытались программировать на Python так, будто это C или Java:
my_items = ['a', 'b', 'c'] i = 0 while i < len(my_items): print(my_items[i]) i += 1
Итак, вы спрашиваете, что же такого непитоновского в этом фрагменте кода?
Две вещи.
Во-первых, в коде вручную отслеживается индекс i - его инициализация нулем, а затем постепенное увеличение после каждой итерации цикла.
И во-вторых, в коде используется функция len(), которая получает размер контейнера my_items, чтобы определить количество итераций.
В Python можно писать циклы, которые справляются с этими двумя задачами автоматически. И будет просто замечательно, если вы возьмете это на вооружение. Например, если вашему коду не придется отслеживать нарастающий индекс, то будет намного труднее написать непреднамеренный бесконечный цикл. Это также сделает программный код более сжатым и поэтому удобочитаемым.
Чтобы рефакторизовать первый пример кода, начнем с того, что удалим фрагмент, который вручную обновляет индекс. В Python лучше всего для этого применить цикл for. При помощи встроенной фабричной функции range() можно генерировать индексы автоматически:
>>> range(len(my_items)) range(0, 3) >>> list(range(0, 3)) [0, 1, 2]
Тип range представляет неизменяемую последовательность чисел. Его преимущество перед обычным списком list в том, что он всегда занимает одинаково небольшое количество оперативной памяти. Объекты-диапазоны в действительности не хранят отдельные значения, представляющие числовую последовательность, вместо этого они функционируют как итераторы и вычисляют значения последовательности на ходу.
Чтобы получить такое экономное для оперативной памяти поведение в Python 2, вам придется использовать встроенную функцию xrange(), так
как функция range() будет в действительности конструировать объект-список.
Поэтому, вместо того чтобы на каждой итерации цикла вручную увеличивать индекс i, можно воспользоваться функцией range() и написать что-то подобное:
for i in range(len(my_items)): print(my_items[i])
Уже лучше. Однако этот вариант по-прежнему выглядит не совсем по- питоновски и ощущается больше как итеративная Java-конструкция, а не как настоящий цикл Python. Когда вы видите программный код, в котором для итеративного обхода контейнера используется range(len(...)), его, как правило, можно еще больше упростить и улучшить.
Как уже отмечалось, циклы for в Python в действительности являются циклами "for each", которые могут выполнять непосредственный перебор элементов контейнера или последовательности без необходимости искать их по индексу. И этот факт можно задействовать для дальнейшего упрощения этого цикла:
for item in my_items: print(item)
Такое решение можно считать вполне питоновским. В нем применено несколько продвинутых функциональных средств Python, но при этом оно остается хорошим и чистым и читается почти как псевдокод из учебника по программированию. Обратите внимание, что в этом цикле больше не отслеживается размер контейнера, а для доступа к элементам не используется нарастающий индекс.
Теперь контейнер сам занимается раздачей элементов для их обработки. Если контейнер упорядочен, то и результирующая последовательность элементов будет такой же. Если контейнер не упорядочен, он будет возвращать свои элементы в произвольном порядке, но цикл по-прежнему охватит их все полностью.
Нужно сказать, что, конечно, вы не всегда будете в состоянии переписать свои циклы таким образом. А что, если, например, вам нужен индекс элемента?
Для таких случаев есть возможность писать циклы, которые поддерживают нарастающий индекс, избегая применения шаблона с range(len(...)). Встроенная функция enumerate() поможет вам сделать подобного рода циклы безупречными и питоновскими:
for i, item in enumerate(my_items): print(f'{i}: {item}') 0: a 1: b 2: c
Дело в том, что итераторы в Python могут возвращать более одного значения. Они могут возвращать кортежи с произвольным числом значений, которые затем могут быть распакованы прямо внутри инструкции for.
Это очень мощное средство. Например, тот же самый прием можно использовать, чтобы в цикле одновременно перебрать ключи и значения словаря:
>>> emails = {
'Боб': 'bob@example.com',
'Алиса': 'alice@example.com',
}
>>> for name, email in emails.items():
print(f'{name} -> {email}')
Боб -> bob@example.com
Алиса -> alice@example.com
Есть еще один пример, который хотелось бы вам показать. Что, если вам совершенно точно нужно написать C-подобный цикл? Например, если вам требуется управлять размером шага индекса? Предположим, что вы начали со следующего цикла Java:
for (int i = a; i < n; i += s) {
// ...
}
Как этот шаблон перевести на Python? И снова на выручку приходит функция range() - она принимает необязательные параметры, которые управляют начальным значением (a), конечным значением (n) и размером шага (s) цикла. Перевод с Java на Python будет выглядеть так:
for i in range(a, n, s): # ...
На следующем шаге мы подитожим изученный материал.