предисловие
Это седьмая статья из серии "Мастер Python".[Посмотреть все статьи цикла]
Цикл — это часто используемая структура управления программой. Мы часто говорим, что одно из самых больших преимуществ машин перед людьми заключается в том, что машины могут делать что-то многократно без сна, а люди — нет. и"цикл", является ключевой концепцией, которая позволяет машине повторять свою работу.
В области грамматики традиция Python Performance, которая не традиционная. Пока он забросил общийfor (init; condition; incrment)Трехступенчатая структура, но все еще выбралаforиwhileЭти два классических ключевых слова используются для обозначения петель. В подавляющем большинстве случаев наши круговые потребности могут быть использованыfor <item> in <iterable>удовлетворить,while <condition>Для сравнения, используется меньше.
Хотя синтаксис цикла прост, его нелегко написать хорошо. В этой статье мы рассмотрим, что такое «аутентичный» циклический код и как его написать.
Что такое «аутентичный» цикл?
Слово «аутентичное» обычно используется для описания кого-то, чтобы сделать что-то, очень хорошо в соответствии с местными традициями и очень хорошо. Например, вы пойдите на вечеринку друг, в одном столе есть кантон, а другая сторона открыта, предложение является стандартной Пекинской камерой, а идеальный звук идеален. Тогда вы можете сказать ей: «Ваш Пекин сказал правдутипичный".
Поскольку слово «аутентичный» часто используется для описания реальных вещей, таких как акценты и вкус приготовления, что означает код «аутентичного» цикла? Поясню на классическом примере.
Если вы спросите кого-то, кто только что изучил Python в течение месяца: "Как я могу получить текущий индекс при обходе списка?Он может передать такой код:
index = 0
for name in names:
print(index, name)
index += 1
Петля выше верна, но она совсем не «аутентична». Человек с трехлетним опытом разработки на Python сказал бы, что код должен быть написан так:
for i, name in enumerate(names):
print(i, name)
enumerate()это встроенная функция Python, которая принимает «итерируемый» объект в качестве параметра и возвращает(当前下标, 当前元素)Новый повторяемый объект . Это самое подходящее для этой сцены.
Таким образом, в приведенном выше примере мы считаем второй код цикла более «аутентичным», чем первый. Потому что он делает работу умнее с более интуитивно понятным кодом.
Идея программирования, представленная enumerate()
Однако знание или незнание встроенного метода — не единственный критерий для оценки того, является ли фрагмент кода цикла идиоматическим. Мы можем копнуть глубже из приведенного выше примера.
Как видите, Pythonforтолько петляfor <item> in <iterable>Такая структура, и первая половина структуры -присвоить элементу- Не слишком много трюков, чтобы играть. Итак, вторая половинаповторяемый объектЭто единственное, из-за чего мы можем поднять большой шум. И сenumerate()*"Функция украшения"*, представленная функцией, просто дает идею:Оптимизируйте сам цикл, украсив итерируемый объект.
Это подводит меня к моему первому предложению.
Предложение 1: используйте функцию для украшения итерируемого объекта, чтобы оптимизировать цикл
Работа с итерируемыми объектами с декорированными функциями может по-разному влиять на циклический код. И чтобы найти подходящий пример для демонстрации этого метода, не нужно далеко ходить, встроенный модульitertoolsявляется прекрасным примером.
В двух словах, itertools — это набор служебных функций, ориентированных на итерацию. Моя предыдущая серия статей«Контейнерный дверной проем»упомянул об этом.
Если вы хотите изучить itertools, тоОфициальная документация по PythonЭто ваш первый выбор, есть очень подробная информация, связанная с модулем. Но в этой статье акцент будет немного отличаться от официальной документации. Я подробно объясню, как это улучшает зацикливание кода в некоторых распространенных сценариях кодирования.
1. Используйте продукт, чтобы сгладить несколько вложенных петель
Хотя все мы знаем *"плоский код лучше вложенного"*. Но иногда для определенных типов потребностей кажется, что вы должны написать несколько слоев вложенных циклов. Например, следующий абзац:
def find_twelve(num_list1, num_list2, num_list3):
"""从 3 个数字列表中,寻找是否存在和为 12 的 3 个数
"""
for num1 in num_list1:
for num2 in num_list2:
for num3 in num_list3:
if num1 + num2 + num3 == 12:
return num1, num2, num3
Для такого кода многоуровневого цикла, который должен вкладывать и обходить несколько объектов, мы можем использоватьproduct()функцию для его оптимизации.product()Можно получить несколько итераций, а затем непрерывно генерировать результаты на основе их декартова произведения.
from itertools import product
def find_twelve_v2(num_list1, num_list2, num_list3):
for num1, num2, num3 in product(num_list1, num_list2, num_list3):
if num1 + num2 + num3 == 12:
return num1, num2, num3
По сравнению с предыдущим кодом используйтеproduct()Функция выполняет задачу только с одним циклом for, и код становится более совершенным.
2. Используйте islice для реализации чередования в цикле
Существует внешний файл данных, содержащий заголовок поста Reddit, Формат контента такой:
python-guide: Python best practices guidebook, written for humans.
---
Python 2 Death Clock
---
Run any Python Script with an Alexa Voice Command
---
<... ...>
Вероятно, для эстетики между каждыми двумя заголовками в этом документе есть"---"разделитель. Теперь нам нужно получить список всех заголовков в файле, поэтому в процессе обхода содержимого файла эти бессмысленные разделители нужно пропускать.
обратитесь к предыдущемуenumerate()Чтобы понять функцию, мы можем добавить абзац на основе текущего порядкового номера цикла в цикл.ifРешение сделать это:
def parse_titles(filename):
"""从隔行数据文件中读取 reddit 主题名称
"""
with open(filename, 'r') as fp:
for i, line in enumerate(fp):
# 跳过无意义的 '---' 分隔符
if i % 2 == 0:
yield line.strip()
Но для такого рода нужно чередование внутри цикла, если использовать itertoolsislice()Функции украшают зацикленный объект, чтобы сделать код тела цикла более простым и прямым.
islice(seq, start, end, step)Функции и операции нарезки массива* ( list[start:stop:step] )имеют практически одинаковые параметры. Если вам нужно выполнить чересстрочную обработку внутри цикла, просто установите значение шага параметра третьего шага приращения на 2.(по умолчанию 1)*.
from itertools import islice
def parse_titles_v2(filename):
with open(filename, 'r') as fp:
# 设置 step=2,跳过无意义的 '---' 分隔符
for line in islice(fp, 0, None, 2):
yield line.strip()
3. Используйте takewhile вместо оператора break
Иногда нам нужно определить, должен ли цикл завершаться преждевременно в начале каждого цикла. Например следующее:
for user in users:
# 当第一个不合格的用户出现后,不再进行后面的处理
if not is_qualified(user):
break
# 进行处理 ... ...
Для таких циклов, которые необходимо прервать досрочно, мы можем использоватьtakewhile()функция для упрощения.takewhile(predicate, iterable)будет повторятьсяiterableПроцесс постоянно вызывается с текущим объектом в качестве параметраpredicateфункцию и проверьте возвращаемый результат, если возвращаемое значение функции истинно, генерируется текущий объект и цикл продолжается. В противном случае текущая петля немедленно прерывается.
использоватьtakewhileпример кода:
from itertools import takewhile
for user in takewhile(is_qualified, users):
# 进行处理 ... ...
В itertools есть и другие интересные служебные функции, которые можно использовать в сочетании с циклами, например, использование функции цепочки для выравнивания двойного вложенного цикла, использование функции zip_longest для одновременного обхода нескольких объектов и т. д.
Из-за ограниченного места я не буду представлять их здесь по одному. Если вам интересно, вы можете перейти к официальной документации, чтобы узнать больше об этом.
4. Напишите свои собственные оформленные функции с помощью генераторов
В дополнение к тем функциям, которые предоставляет itertools, мы также можем легко использовать генераторы для определения наших собственных функций оформления циклов.
Возьмем пример простой функции:
def sum_even_only(numbers):
"""对 numbers 里面所有的偶数求和"""
result = 0
for num in numbers:
if num % 2 == 0:
result += num
return result
В приведенной выше функции, чтобы отфильтровать все нечетные числа, тело цикла вводит дополнительныйifСудите приговоры. Если мы хотим упростить тело цикла, мы можем определить функцию-генератор, которая специально выполняет даже фильтрацию:
def even_only(numbers):
for num in numbers:
if num % 2 == 0:
yield num
def sum_even_only_v2(numbers):
"""对 numbers 里面所有的偶数求和"""
result = 0
for num in even_only(numbers):
result += num
return result
будетnumbersиспользование переменныхeven_onlyПосле оформления функцииsum_even_only_v2Внутри функции вам не нужно продолжать обращать внимание на логику «четного фильтра», вам нужно просто выполнить суммирование.
Подсказка: Конечно, вышеуказанная функция непрактична. В реальном мире это простое требование лучше всего выполнять непосредственно с помощью выражений генератора/списка:
sum(num for num in numbers if num % 2 == 0)
Рекомендация 2: Разбивайте сложные блоки кода в теле цикла по ответственности
Я всегда думал, что циклы — это какая-то волшебная штука, каждый раз, когда вы пишете новый блок кода цикла, это как открытие черного магического круга, и все в круге начнет повторяться бесконечно.
Но в то же время я обнаружил, что помимо пользы от этого черного магического круга,Это также побуждает вас продолжать добавлять в массив все больше и больше кода, включая фильтрацию недопустимых элементов, предварительную обработку данных, печать журналов и многое другое. Даже некоторый контент, который изначально не относился к одной и той же абстракции, будет запихнут в тот же черный магический круг.
Вы можете считать это само собой разумеющимся, нам просто отчаянно нужна магия массива. Если не впихивать всю эту логику в тело цикла, куда еще ее можно поместить?
Рассмотрим следующий бизнес-сценарий. На веб-сайте есть повторяющийся скрипт, который выполняется каждые 30 дней. Его задача — опрашивать пользователей, которые заходили в систему в течение определенного периода времени каждые выходные за последние 30 дней, а затем отправлять им призовые баллы.
код показывает, как показано ниже:
import time
import datetime
def award_active_users_in_last_30days():
"""获取所有在过去 30 天周末晚上 8 点到 10 点登录过的用户,为其发送奖励积分
"""
days = 30
for days_delta in range(days):
dt = datetime.date.today() - datetime.timedelta(days=days_delta)
# 5: Saturday, 6: Sunday
if dt.weekday() not in (5, 6):
continue
time_start = datetime.datetime(dt.year, dt.month, dt.day, 20, 0)
time_end = datetime.datetime(dt.year, dt.month, dt.day, 23, 0)
# 转换为 unix 时间戳,之后的 ORM 查询需要
ts_start = time.mktime(time_start.timetuple())
ts_end = time.mktime(time_end.timetuple())
# 查询用户并挨个发送 1000 奖励积分
for record in LoginRecord.filter_by_range(ts_start, ts_end):
# 这里可以添加复杂逻辑
send_awarding_points(record.user_id, 1000)
Вышеупомянутая функция в основном состоит из двух слоев циклов. Ответственность внешнего цикла в основном состоит в том, чтобы получить время, соответствующее требованиям, за последние 30 дней и преобразовать его в отметку времени UNIX. Эти две метки времени затем используются внутренним циклом для целочисленной отправки.
Как я уже говорил, черный магический круг, открытый внешней циркуляцией, был заполнен. Но после наблюдения мы можем обнаружить, чтоВесь цикл на самом деле состоит из двух совершенно не связанных между собой задач: «выбрать дату и подготовить отметку времени» и «отправить бонусные баллы»..
Как сложные циклы реагируют на новые требования
Что плохого в таком коде? позвольте мне сказать вам.
В один прекрасный день продукт пришел и сообщил, что некоторые пользователи не спят среди ночи на выходных и все еще просматривают наш сайт, нам нужно было отправить им уведомление, чтобы в будущем они могли ложиться спать раньше. Так появились новые требования:"Отправить уведомление пользователям, которые заходили в систему с 3:00 до 5:00 по выходным за последние 30 дней".
Далее следуют новые проблемы. Как бы вы ни были увлечены, вы можете с первого взгляда обнаружить, что требования этого нового требования в разделе проверки пользователей очень похожи на предыдущие требования. Однако, если вы откроете тело цикла раньше, вы обнаружите, что код вообще нельзя использовать повторно, потому что внутри цикла совершенно другая логика.связьвместе. ☹️
В компьютерном мире мы часто используем слово «связь» для обозначения связи между вещами. В приведенном выше примере *"время выбора"и«Точки отправки»* Эти две вещи находятся в одном цикле, создавая очень сильную взаимосвязь.
Для лучшего повторного использования кода нам нужно отделить часть функции «время выбора» от тела цикла. И наш старый друг, «функции генератора», — это то, что нужно для работы.
Развязка тел циклов с генераторными функциями
положить«Выбери время»частично отделены от цикла, нам нужно определить новую функцию-генераторgen_weekend_ts_ranges(), специально для создания необходимой временной метки UNIX:
def gen_weekend_ts_ranges(days_ago, hour_start, hour_end):
"""生成过去一段时间内周六日特定时间段范围,并以 UNIX 时间戳返回
"""
for days_delta in range(days_ago):
dt = datetime.date.today() + datetime.timedelta(days=days_delta)
# 5: Saturday, 6: Sunday
if dt.weekday() not in (5, 6):
continue
time_start = datetime.datetime(dt.year, dt.month, dt.day, hour_start, 0)
time_end = datetime.datetime(dt.year, dt.month, dt.day, hour_end, 0)
# 转换为 unix 时间戳,之后的 ORM 查询需要
ts_start = time.mktime(time_start.timetuple())
ts_end = time.mktime(time_end.timetuple())
yield ts_start, ts_end
С помощью этой функции генератора старое требование «отправить бонусные баллы» и новое требование «отправить уведомление» можно повторно использовать в цикле для выполнения задачи:
def award_active_users_in_last_30days_v2():
"""发送奖励积分"""
for ts_start, ts_end in gen_weekend_ts_ranges(30, hour_start=20, hour_end=23):
for record in LoginRecord.filter_by_range(ts_start, ts_end):
send_awarding_points(record.user_id, 1000)
def notify_nonsleep_users_in_last_30days():
"""发送通知"""
for ts_start, ts_end in gen_weekend_ts_range(30, hour_start=3, hour_end=6):
for record in LoginRecord.filter_by_range(ts_start, ts_end):
notify_user(record.user_id, 'You should sleep more')
Суммировать
В этой статье мы впервые кратко объяснили определение «аутентичного» кода циклов. Затем последовало первое предложение: использовать декорированные функции для улучшения циклов. Затем я создал виртуальный бизнес-сценарий, описывающий важность разбивки кода внутри цикла по ответственности.
Несколько ключевых моментов для обобщения:
- Использование функции для украшения самого зацикленного объекта может улучшить код в теле цикла.
- В itertools есть много полезных функций, которые можно использовать для улучшения циклов.
- Использование функций-генераторов упрощает определение собственных оформленных функций.
- Внутри цикла это место, склонное к «раздуванию кода».
- Пожалуйста, используйте функции генератора, чтобы отделить блоки кода с разными обязанностями в цикле для большей гибкости.
После прочтения статьи у вас есть на что пожаловаться? Пожалуйста, оставьте сообщение или наПроблемы проекта GitHubскажите мне.
приложение
- Источник титульного изображения: Фото Лай Ман Нуна на Unsplash
- Еще серия адресов статей:GitHub.com/leather/one-…
Другие статьи цикла: