Шаблоны циклов в Python: подсчёт, сумма, максимум, минимум

Когда я только осваивал Python, задача "найти максимум в списке" казалась тривиальной, пока не пришлось обрабатывать данные без встроенных функций. Написав пять циклов за час, я заметил, что каждый из них следует одной и той же структуре: переменная-аккумулятор перед циклом, обновление на каждой итерации, результат после цикла. Это и есть шаблоны циклов (loop patterns): повторяющиеся конструкции для подсчёта, суммирования, поиска максимума и минимума.
Статья разбирает четыре базовых шаблона, показывает их реализацию вручную и через встроенные функции Python, сравнивает подходы и объясняет, как комбинировать шаблоны в одном цикле.
Что такое шаблон цикла и зачем он нужен
Шаблон цикла (loop pattern) это повторяющаяся структура кода, где переменная-аккумулятор накапливает результат по мере прохождения элементов коллекции. Каждый шаблон состоит из трёх частей: инициализация аккумулятора до цикла, обновление аккумулятора внутри цикла, использование результата после цикла.
Шаблоны полезны по двум причинам. Первая: они превращают задачу обработки данных в предсказуемый алгоритм. Вторая: понимание шаблонов помогает выбрать правильную встроенную функцию Python, потому что len(), sum(), max() и min() реализуют те же самые паттерны внутри.
Общая структура
# 1. Инициализация аккумулятора
accumulator = начальное_значение
# 2. Цикл с обновлением
for element in collection:
accumulator = обновление(accumulator, element)
# 3. Результат
print(accumulator)
Меняется только начальное значение и логика обновления. Всё остальное одинаково для подсчёта, суммирования, максимума и минимума.
Шаблон подсчёта (counting)
Подсчёт отвечает на вопрос "сколько элементов в коллекции?" или "сколько элементов удовлетворяют условию?". Аккумулятор инициализируется нулём и увеличивается на 1 при каждой итерации (или при выполнении условия).
Подсчёт всех элементов
count = 0
for itervar in [3, 41, 12, 9, 74, 15]:
count = count + 1
print("Количество:", count)
# Количество: 6
Переменная count начинается с 0. На каждой итерации значение itervar не используется, count просто растёт на 1. После цикла count содержит число элементов.
Подсчёт по условию
Задача: посчитать, сколько чисел в списке больше 10.
numbers = [3, 41, 12, 9, 74, 15]
count = 0
for num in numbers:
if num > 10:
count += 1
print("Больше 10:", count)
# Больше 10: 4
Здесь count увеличивается не на каждой итерации, а только когда num > 10. Это расширение базового шаблона условным оператором.
Подсчёт символов в строке
word = "banana"
count = 0
for letter in word:
if letter == "a":
count += 1
print(count)
# 3
Строка в Python итерируема, поэтому for перебирает её посимвольно. Шаблон тот же: инициализация 0, условие, инкремент.
Замена встроенной функцией
numbers = [3, 41, 12, 9, 74, 15]
print(len(numbers)) # 6
# Подсчёт по условию через генераторное выражение
print(sum(1 for num in numbers if num > 10)) # 4
Функция len() заменяет простой подсчёт. Для подсчёта по условию удобна конструкция sum(1 for ...), где генератор выдаёт 1 для каждого подходящего элемента.
Шаблон суммирования (summing)
Суммирование отвечает на вопрос "какова общая сумма элементов?". Аккумулятор инициализируется нулём и на каждой итерации увеличивается на значение текущего элемента.
Базовое суммирование
total = 0
for itervar in [3, 41, 12, 9, 74, 15]:
total = total + itervar
print("Сумма:", total)
# Сумма: 154
Переменная total начинается с 0. На каждой итерации к ней прибавляется itervar. Пошаговое выполнение:
total = 0 + 3 = 3
total = 3 + 41 = 44
total = 44 + 12 = 56
total = 56 + 9 = 65
total = 65 + 74 = 139
total = 139 + 15 = 154
Суммирование с условием
Задача: сложить только положительные числа.
data = [10, -3, 25, -7, 8, 0, -1]
positive_sum = 0
for num in data:
if num > 0:
positive_sum += num
print("Сумма положительных:", positive_sum)
# Сумма положительных: 43
Среднее арифметическое: комбинация подсчёта и суммирования
numbers = [3, 41, 12, 9, 74, 15]
count = 0
total = 0
for num in numbers:
count += 1
total += num
average = total / count
print("Среднее:", average)
# Среднее: 25.666666666666668
Два аккумулятора работают в одном цикле. count считает элементы, total складывает значения. После цикла делим одно на другое.
Замена встроенной функцией
numbers = [3, 41, 12, 9, 74, 15]
print(sum(numbers)) # 154
print(sum(numbers) / len(numbers)) # 25.666...
Функция sum() принимает итерируемый объект и необязательный аргумент start (по умолчанию 0).
Шаблон поиска максимума
Поиск максимума отвечает на вопрос "какой элемент самый большой?". Аккумулятор инициализируется значением None и обновляется, когда текущий элемент больше хранимого.
Базовый поиск максимума
largest = None
print("До цикла:", largest)
for itervar in [3, 41, 12, 9, 74, 15]:
if largest is None or itervar > largest:
largest = itervar
print("Итерация:", itervar, largest)
print("Максимум:", largest)
Вывод:
До цикла: None
Итерация: 3 3
Итерация: 41 41
Итерация: 12 41
Итерация: 9 41
Итерация: 74 74
Итерация: 15 74
Максимум: 74
На первой итерации largest равен None, поэтому условие largest is None истинно, и largest получает значение 3. На второй итерации 41 > 3, поэтому largest обновляется до 41. Значение 12 меньше 41, обновления нет. На пятой итерации 74 > 41, largest становится 74.
Почему None, а не первый элемент
Инициализация через None безопаснее, чем присваивание первого элемента вручную. Если список пуст, largest остаётся None, и программа не упадёт с ошибкой IndexError. Условие largest is None работает как "страховка" для первой итерации.
Альтернативный вариант с первым элементом:
numbers = [3, 41, 12, 9, 74, 15]
largest = numbers[0] # упадёт, если список пуст
for num in numbers[1:]:
if num > largest:
largest = num
print(largest) # 74
Этот вариант создаёт срез numbers[1:], который копирует часть списка в память. Для больших списков это расточительно.
Замена встроенной функцией
numbers = [3, 41, 12, 9, 74, 15]
print(max(numbers)) # 74
# max с пустым списком
print(max([], default=None)) # None (без default будет ValueError)
Функция max() принимает аргумент default (Python 3.4+), который возвращается при пустом итерируемом объекте.
Шаблон поиска минимума
Поиск минимума зеркален поиску максимума. Единственное отличие: условие обновления меняется с > на <.
Базовый поиск минимума
smallest = None
for itervar in [3, 41, 12, 9, 74, 15]:
if smallest is None or itervar < smallest:
smallest = itervar
print("Минимум:", smallest)
# Минимум: 3
Реализация в виде функции
def find_min(values):
smallest = None
for value in values:
if smallest is None or value < smallest:
smallest = value
return smallest
print(find_min([3, 41, 12, 9, 74, 15])) # 3
print(find_min([])) # None
Эта функция повторяет логику встроенной min() в упрощённом виде.
Сравнение четырёх шаблонов
Подсчёт и суммирование инициализируются нулём, потому что 0 нейтрален для сложения. Максимум и минимум инициализируются None, потому что любое заранее выбранное число может оказаться больше или меньше реального экстремума.
Комбинирование шаблонов в одном цикле
Несколько аккумуляторов могут работать одновременно. Это экономит время: один проход по данным вместо четырёх.
numbers = [3, 41, 12, 9, 74, 15]
count = 0
total = 0
largest = None
smallest = None
for num in numbers:
count += 1
total += num
if largest is None or num > largest:
largest = num
if smallest is None or num < smallest:
smallest = num
print(f"Количество: {count}")
print(f"Сумма: {total}")
print(f"Среднее: {total / count}")
print(f"Максимум: {largest}")
print(f"Минимум: {smallest}")
Вывод:
Количество: 6
Сумма: 154
Среднее: 25.666666666666668
Максимум: 74
Минимум: 3
Когда комбинировать, а когда нет
Комбинирование полезно, когда данные читаются из файла или потока, и повторный проход невозможен или дорог. Если данные уже в списке, вызовы len(), sum(), max(), min() нагляднее и написаны на C, то есть работают быстрее цикла на Python.
Практический пример: статистика оценок студентов
Задача: прочитать оценки от пользователя, вычислить количество, сумму, среднее, максимальную и минимальную оценку.
count = 0
total = 0
largest = None
smallest = None
while True:
line = input("Введите оценку (done для завершения): ")
if line == "done":
break
try:
score = float(line)
except ValueError:
print("Некорректный ввод, попробуйте снова")
continue
count += 1
total += score
if largest is None or score > largest:
largest = score
if smallest is None or score < smallest:
smallest = score
if count > 0:
print(f"Оценок: {count}")
print(f"Сумма: {total}")
print(f"Среднее: {total / count:.2f}")
print(f"Максимум: {largest}")
print(f"Минимум: {smallest}")
else:
print("Оценки не введены")
Здесь объединены шаблон while True с break, обработка ошибок через try/except с continue и все четыре аккумулятора. Проверка count > 0 защищает от деления на ноль.
Шаблоны за пределами чисел: строки и словари
Аккумуляторный паттерн применим не только к числам.
Конкатенация строк
words = ["Python", "это", "просто"]
result = ""
for word in words:
result = result + word + " "
print(result.strip())
# Python это просто
Аккумулятор result инициализируется пустой строкой. На каждой итерации к нему прибавляется слово с пробелом. Питоничнее использовать " ".join(words), но паттерн тот же.
Подсчёт частоты символов (гистограмма)
word = "brontosaurus"
counts = {}
for char in word:
counts[char] = counts.get(char, 0) + 1
print(counts)
# {'b': 1, 'r': 2, 'o': 2, 'n': 1, 't': 1, 's': 2, 'a': 1, 'u': 2}
Аккумулятор здесь словарь. Метод dict.get(key, default) возвращает текущее значение или 0, если ключ отсутствует. Это шаблон подсчёта с группировкой по ключу.
Сбор элементов по условию (фильтрация в список)
numbers = [3, 41, 12, 9, 74, 15]
big = []
for num in numbers:
if num > 20:
big.append(num)
print(big) # [41, 74]
Аккумулятор big это пустой список. Шаблон подсчёта заменён на шаблон накопления: вместо += 1 используется append().
Ручной цикл vs встроенные функции: что выбрать
Рекомендация: если данные в списке и нужна одна агрегация, используйте len(), sum(), max(), min(). Если нужно несколько агрегаций за один проход или данные читаются из потока, пишите ручной цикл с несколькими аккумуляторами.
Неочевидные детали
Первый факт: sum() принимает второй аргумент start. По умолчанию start=0, но можно передать другое значение. Например, sum([1, 2, 3], 10) вернёт 16.
Второй факт: max() и min() принимают аргумент key, который задаёт функцию сравнения. Например, max(words, key=len) вернёт самое длинное слово. Ручной цикл для этого потребует двух аккумуляторов: один для максимальной длины, второй для самого слова.
words = ["cat", "elephant", "dog", "hippopotamus"]
print(max(words, key=len)) # hippopotamus
print(min(words, key=len)) # cat
Третий факт: для вычисления произведения в Python 3.8+ появилась math.prod():
import math
numbers = [2, 3, 4, 5]
print(math.prod(numbers)) # 120
До Python 3.8 использовали functools.reduce():
from functools import reduce
from operator import mul
print(reduce(mul, [2, 3, 4, 5])) # 120
Четвёртый факт: collections.Counter реализует шаблон подсчёта частоты в одну строку:
from collections import Counter
word = "brontosaurus"
print(Counter(word))
# Counter({'r': 2, 'o': 2, 's': 2, 'u': 2, 'b': 1, 'n': 1, 't': 1, 'a': 1})
FAQ
Почему максимум и минимум инициализируют через None, а не через float('inf')?
Оба варианта рабочие. None безопаснее, потому что подходит для любого типа данных: строки, кортежи, пользовательские объекты. Значение float('inf') работает только с числами. При инициализации через None первый элемент всегда записывается в аккумулятор.
Можно ли использовать enumerate() в шаблонах подсчёта?
Да. Функция enumerate() возвращает пары (индекс, элемент). Но для простого подсчёта проще использовать len(). enumerate() полезен, когда нужен и индекс, и значение одновременно.
Как найти индекс максимального элемента?
Через ручной цикл или встроенные функции:
numbers = [3, 41, 12, 74, 15]
# Вариант 1: ручной цикл
max_idx = 0
for i in range(1, len(numbers)):
if numbers[i] > numbers[max_idx]:
max_idx = i
print(max_idx) # 3
# Вариант 2: встроенные функции
print(numbers.index(max(numbers))) # 3
Работают ли шаблоны с генераторами?
Да. Функции sum(), max(), min() принимают генераторные выражения. Генератор не создаёт список в памяти, а выдаёт элементы по одному:
print(sum(x**2 for x in range(1000)))
Зачем писать шаблоны вручную, если есть встроенные функции?
Понимание шаблонов помогает решать задачи, для которых нет готовой функции: поиск второго максимума, скользящее среднее, подсчёт серий одинаковых элементов. Встроенные функции покрывают простые случаи, но нестандартные задачи требуют ручных аккумуляторов.
Мой совет: запомните структуру "инициализация, цикл, результат" и научитесь определять, какой тип обновления нужен. Дальше выбор между ручным циклом и встроенной функцией станет очевидным: одна агрегация по готовому списку это len/sum/max/min, всё остальное это цикл с аккумуляторами.
