Learning Platform
Урок 04.05 · 22 мин
Начальный
Control flowifforwhilematchWalrus operator
Протокол итератора: как работает for под капотом

Что мы уже умеем и что осталось

Мы научились хранить данные — в переменных и коллекциях. Превращать списки одним выражением — через 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. Блоки выделяются

отступами
. 4 пробела — стандарт. Все строки одного блока должны быть с одинаковым отступом, иначе IndentationError.

Тернарный оператор

Однострочное условие:

status = "adult" if age >= 18 else "minor"

Читается естественно: «adult если age ≥ 18, иначе minor». Удобно для коротких присваиваний, не злоупотребляйте на сложных условиях.

Truthiness — что считается ложью

Python в условиях принимает не только bool, но любой объект. Каждый объект имеет «истинностное» значение. Запоминаем, что считается ложью:

  • False
  • None
  • 0, 0.0, 0j
  • пустая строка ""
  • пустой список [], пустой словарь {}, пустой set set(), пустой tuple ()
  • пустой bytes b""
  • объекты, у которых определён __bool__ или __len__, возвращающий False/0

Всё остальное — истина. В том числе "False" (непустая строка!), [0] (непустой список с нулём внутри!).

Что Python считает ложью

Любое из этого значение в `if` войдёт в else.

Falsy valuesоцениваются как False
Falsebool
NoneNoneType
0, 0.0числа
""пустая строка
продолжение
[]пустой list
{}пустой dict
set()пустой set
()пустой tuple
Truthyвсё остальное
"False"непустая строкаСлово 'False' — это строка из 5 символов, она truthy!
[0]непустой listНе путать с []. Содержит элемент 0, но сам список непустой.
datetime(...)любой объект

Это позволяет писать естественно:

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")
WARNING

Если поле может быть 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 42

while

Бесконечный цикл с явным выходом:

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 — это

structural pattern matching
: можно разбирать вложенные структуры данных, привязывать переменные, проверять формы.

Простейший случай — как 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 "в начале координат"
TIP

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.

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 8