Что мы уже умеем и что осталось
Мы научились хранить данные — в переменных и коллекциях. Превращать списки одним выражением — через comprehensions. Но всё это пока без «логики». Без условий и циклов скрипт не сделает ни одного интересного действия. В этом уроке закрываем тему управляющих конструкций.
Если ты никогда не писал код: что такое условие и цикл
Эти две идеи — основа любой программы, и они проще, чем звучат. С самими понятиями ты уже сталкивался в модуле 1 (Первые шаги); здесь мы дадим им имена и аналогии.
- Условие (ветвление) — это развилка на дороге. «Если светофор зелёный — едем, иначе стоим». Программа смотрит на какое-то значение и выбирает одну из веток. В Python развилку задаёт
if(«если»),elif(«иначе если») иelse(«иначе»). - Цикл — это «повторяй действие, пока выполняется условие» или «сделай что-то для каждого элемента». Аналогия: проверить почтовый ящик и достать каждое письмо по очереди, пока ящик не опустеет. В Python для этого есть
while(повторять, пока верно условие) иfor(для каждого элемента коллекции).
Если эти аналогии понятны — дальше всё ляжет легко: ниже мы просто показываем, как они записываются в Python и в чём его особенности.
Дальше — специфика Python: где он ведёт себя иначе, чем привычно, какие есть фишки 3.8+ (walrus) и 3.10+ (match/case), и какие ловушки специфичны именно для него.
if / elif / else
country = "RU"
if country == "RU":
print("Россия")
elif country == "BY":
print("Беларусь")
elif country in {"KZ", "KG", "UZ"}:
print("Центральная Азия")
else:
print("Другая страна")
Никаких скобок, никаких then. Блоки выделяются
IndentationError.
Тернарный оператор
Однострочное условие:
status = "adult" if age >= 18 else "minor"
Читается естественно: «adult если age ≥ 18, иначе minor». Удобно для коротких присваиваний, не злоупотребляйте на сложных условиях.
Truthiness — что считается ложью
Python в условиях принимает не только bool, но любой объект. Каждый объект имеет «истинностное» значение. Запоминаем, что считается ложью:
FalseNone0,0.0,0j- пустая строка
"" - пустой список
[], пустой словарь{}, пустой setset(), пустой tuple() - пустой bytes
b"" - объекты, у которых определён
__bool__или__len__, возвращающийFalse/0
Всё остальное — истина. В том числе "False" (непустая строка!), [0] (непустой список с нулём внутри!).
Любое из этого значение в `if` войдёт в else.
Это позволяет писать естественно:
records: list[dict] = []
if records: # эквивалент: if len(records) > 0
process(records)
else:
print("Нет данных")
name: str | None = get_name()
if not name: # эквивалент: name is None or name == ""
raise ValueError("name is empty")
Если поле может быть 0, а вы пишете if value: — 0 уйдёт в else. Для чисел и других «логически пустых» значений используйте явную проверку is None:
# плохо: 0 ушёл в else
def with_default(x: int | None) -> int:
return x or 42 # если x=0, вернёт 42, не 0!
# хорошо
def with_default(x: int | None) -> int:
return x if x is not None else 42while
Бесконечный цикл с явным выходом:
attempt = 0
while attempt < 3:
if try_connect():
break
attempt += 1
else:
raise ConnectionError("Не смог подключиться за 3 попытки")
Обратите внимание на else у цикла — выполняется, если цикл завершился без break. Не путать с else у if. Это редкая фишка Python, многие про неё забывают. Использовать осторожно — не каждый коллега её знает.
for и итерация
Python for — это не C-style for(i=0; i<n; i++). Это всегда обход итерируемого объекта:
# список
for x in [1, 2, 3]:
print(x)
# словарь — по ключам по умолчанию
for k in {"a": 1, "b": 2}:
print(k)
# словарь — по парам
for k, v in {"a": 1, "b": 2}.items():
print(f"{k}={v}")
# строка — по символам
for ch in "hello":
print(ch)
# числа в диапазоне
for i in range(5): # 0, 1, 2, 3, 4
print(i)
for i in range(2, 10, 2): # 2, 4, 6, 8 — start, stop, step
print(i)
# с индексом — через enumerate
for i, name in enumerate(["Анна", "Борис", "Вера"]):
print(f"{i}: {name}")
# 0: Анна
# 1: Борис
# 2: Вера
# параллельная итерация — через zip
names = ["Анна", "Борис"]
ages = [30, 25]
for name, age in zip(names, ages):
print(f"{name}: {age}")
enumerate и zip — два инструмента, которые используются в каждом DE-скрипте. Запомните.
Что значит «итерируемое»
Python вызывает на объекте магический метод
__iter__()__next__() пока не получит исключение StopIteration. Концептуально:
# то, что пишем
for x in xs:
do(x)
# то, что внутри делает Python:
it = iter(xs)
while True:
try:
x = next(it)
except StopIteration:
break
do(x)
Это знание понадобится, когда дойдём до генераторов и потоковой обработки больших файлов (модуль 4). Пока просто помните: for дёргает «следующий элемент» — он не знает заранее, сколько их будет.
break и continue
# найти первое совпадение
for r in records:
if r["id"] == target:
result = r
break # выйти из цикла
else:
result = None # else-клаузу не выполняем, был break
# пропустить текущий элемент
for r in records:
if r.get("deleted"):
continue # перейти к следующей итерации
process(r)
for ... else — то же поведение, что и в while. Выполняется, если цикл завершился без break. Идиоматично для «искал и не нашёл»:
for line in log_lines:
if "ERROR" in line:
first_error = line
break
else:
first_error = None
match / case — pattern matching (3.10+)
Появилось в Python 3.10 (октябрь 2021), позаимствовано из функциональных языков. Это не обычный switch — это
Простейший случай — как switch:
def http_status_label(code: int) -> str:
match code:
case 200:
return "OK"
case 404:
return "Not Found"
case 500:
return "Internal Server Error"
case _:
return "Unknown"
print(http_status_label(404)) # "Not Found"
Где _ — wildcard, ловит всё, что не подошло.
Самое полезное для DE — сопоставление по форме:
def describe(data: dict) -> str:
match data:
case {"type": "user", "name": name}:
return f"Пользователь {name}"
case {"type": "order", "amount": amount, "currency": curr}:
return f"Заказ {amount} {curr}"
case {"type": "error", "code": code}:
return f"Ошибка {code}"
case _:
return "Неизвестная структура"
Это не просто проверка ключей — это извлечение значений в переменные. В кейсе {"type": "user", "name": name} мы говорим: «если объект имеет поле type=user и любое поле name, привяжи name к локальной переменной». Это короче и читаемее, чем серия if ... elif.
Можно сопоставлять список:
def first_record(records: list) -> str:
match records:
case []:
return "пусто"
case [single]:
return f"одна запись: {single}"
case [first, *rest]:
return f"первая: {first}, остальных: {len(rest)}"
Можно добавлять if-условия:
match point:
case (x, y) if x == y:
return "на диагонали"
case (x, y) if x == 0:
return "на оси Y"
case (0, 0):
return "в начале координат"
match/case часто заменяет дерево if/elif при разборе ответов API или вложенного JSON. Когда у вас 5+ условий по форме данных — match/case делает код понятнее. До 3 условий — обычный if чище.
Walrus operator := (3.8+)
Новый оператор
:=# было: вызов get_data() дважды
if get_data() is not None:
process(get_data())
# стало: вызов один раз, результат привязан к data
if (data := get_data()) is not None:
process(data)
В цикле:
# чтение файла построчно с условием на пустую строку
with open("data.txt") as f:
while (line := f.readline()):
process(line.strip())
В comprehension — особо полезно, если выражение дорогое:
# плохо — get_features() вычисляется дважды для каждой записи
result = [
get_features(r)
for r in records
if get_features(r) is not None
]
# хорошо — один вызов на запись
result = [
f
for r in records
if (f := get_features(r)) is not None
]
Не злоупотребляйте: walrus делает код плотнее, но иногда непривычным к нему читателям трудно расшифровать. Используйте там, где он действительно экономит вычисление или строку, а не «потому что красиво».
DE-кейс: парсинг ответа API через match
Типовая задача: API возвращает разные структуры в зависимости от типа события, нужно их разобрать.
def process_event(event: dict) -> str:
match event:
case {"type": "page_view", "url": url, "user_id": uid}:
return f"User {uid} viewed {url}"
case {"type": "purchase", "amount": amount, "items": items} if items:
total = f"{amount:.2f}"
return f"Purchase of {len(items)} items for {total}"
case {"type": "purchase", "amount": _, "items": []}:
return "Empty purchase — should be ignored"
case {"type": "signup", "email": email}:
return f"New signup: {email}"
case {"type": ev_type}:
return f"Unknown event type: {ev_type}"
case _:
return "Invalid event structure"
events = [
{"type": "page_view", "url": "/home", "user_id": 42},
{"type": "purchase", "amount": 99.5, "items": [{"sku": "A"}, {"sku": "B"}]},
{"type": "signup", "email": "[email protected]"},
{"type": "weird", "x": 1},
"not even a dict",
]
for e in events:
print(process_event(e))
Эквивалент через if/elif был бы в 2-3 раза длиннее и менее читаемый, потому что каждое условие требовало бы отдельную проверку наличия ключа и извлечения значения.
Упражнение
В REPL:
# 1. Напишите тернарным оператором: вернуть "even" если n чётное, "odd" иначе.
n = 7
# 2. Перепишите циклом for без enumerate, выводя номер и значение
# (счётчик через переменную):
items = ["a", "b", "c"]
# 3. Перепишите то же через enumerate.
# 4. Прочитайте файл "data.txt" построчно через walrus оператор,
# выведите только строки длиннее 10 символов.
# Создайте файл заранее:
with open("data.txt", "w") as f:
f.write("short\nthis is a long enough line\nx\nanother long line for test\n")
# 5. Через match/case разберите следующие структуры:
items = [
{"kind": "circle", "radius": 5},
{"kind": "rectangle", "width": 3, "height": 4},
{"kind": "triangle", "sides": [3, 4, 5]},
{"kind": "unknown"},
]
# Для каждого выведите площадь (для unknown — "не знаю").
Критерии:
- В (4) walrus используется для строки чтения.
- В (5) match покрывает все три фигуры + unknown.
- Нет лишних
if-elifцепочек там, где match читается лучше.
В следующем уроке — функции. Аргументы, type hints, ловушка mutable defaults, лямбды и docstrings.