💻 Практические примеры

⚡ Приёмы практикума на одном экране

import re, json
from datetime import datetime
from collections import defaultdict
from pathlib import Path

digits = re.sub(r"\D", "", "+49 157 1234-5678")            # '4915712345678'  очистка
month = datetime.strptime("06-09-2025", "%d-%m-%Y").month  # 9                дата → месяц
grouped = defaultdict(list); grouped["Q1"].append(86)       # {'Q1': [86]}     авто-ключ
data = json.loads(Path("g.json").read_text(encoding="utf-8"))  # чтение JSON
ТемаИнструментЧто даёт
Очистка строкиre.sub(r"\D", "", s)оставляет только цифры
Поиск шаблонаre.compile(...).findall/finditerтелефоны, email, номера карт
Дата → кварталdatetime.strptime(s, "%d-%m-%Y").monthчисло месяца из строки
Группировкаdefaultdict(list)ключ создаётся сам, без if
Файлы и JSONPath.read_text() + json.loads/dumpsбез ручного open()/close()
Топ-3 ошибки: забыть сырую строку r"..." · перепутать месяцы кварталов (Q1/Q2/Q3) · доверять regex без дополнительной проверки в коде (длина номера, префикс оператора).

Примеры идут от простого к сложному: сначала отдельные приёмы (очистка строки, сборка шаблона, чтение JSON, дата, генератор), затем их связка — так же, как устроены реальные решения в заданиях практикума. Весь вывод в комментариях получен запуском кода, а не подобран по памяти.

Пример 1. re.compile + findall — база всех текстовых заданий

Во всех четырёх заданиях практикума текст сначала «фильтруется» регуляркой. Шаблон стоит скомпилировать один раз через re.compile, если он используется в цикле по строкам файла (см. урок 60 — Регулярные выражения).

import re

text = "Order #123, code: ABC-42, id: 007"
number_re = re.compile(r"\d+")
print(number_re.findall(text))
# ['123', '42', '007']

Что происходит: re.compile создаёт объект шаблона; findall возвращает список всех непересекающихся совпадений. Компиляция один раз вне цикла — так же быстрее, чем вызывать re.findall заново на каждой строке.

Пример 2. Очистка строки: re.sub(r"\D", "", ...)

Первый шаг нормализации номера в задании 2 — не пытаться угадать формат регуляркой, а сначала убрать всё лишнее.

import re

raw = "+49 157 1234-5678"
clean = re.sub(r"\D", "", raw)
print(clean)
# 4915712345678

Что происходит: \D — класс «не цифра» (пробел, дефис, табуляция, плюс). re.sub заменяет каждое совпадение на пустую строку, то есть вырезает его. После такой очистки с номером удобно работать как со строкой цифр — резать по индексам, проверять длину.

Пример 3. Собираем шаблон немецкого номера по частям

Задание 2 описывает номер как последовательность требований: код страны → префикс оператора → 7–9 цифр с необязательными разделителями. Каждое требование — это часть шаблона.

import re

phone_re = re.compile(r"(?:\+49|0049)[\s\-]*1[5-7](?:[\s\-]*\d){7,9}")
sample = "Call +49 157 12345678 or 0049-160-9876543 or +49 89 123456 (landline)"
print(phone_re.findall(sample))
# ['+49 157 12345678', '0049-160-9876543']

Что происходит: (?:\+49|0049) — негруппирующая альтернатива для кода страны (не создаёт отдельный элемент в результате). 1[5-7] — первые две цифры мобильного оператора (15/16/17). (?:[\s\-]*\d){7,9} повторяет «необязательный разделитель + цифра» от 7 до 9 раз. Городской номер +49 89 123456 в результат не попал — после 89 нет оператора 15/16/17.

Пример 4. Валидация после regex — фильтр первого приближения

Шаблон из примера 3 находит кандидатов, но задание 2 требует ещё проверить итоговую длину и привести номер к единому виду +49.... Это и есть приём «regex ищет, код — проверяет и нормализует».

def clean_and_validate(raw_match: str) -> str | None:
    digits = re.sub(r"\D", "", raw_match)
    if digits.startswith("0049"):
        digits = digits[4:]
    elif digits.startswith("49"):
        digits = digits[2:]
    if digits[:2] not in ("15", "16", "17"):
        return None
    if not (10 <= len(digits) <= 12):
        return None
    return f"+49{digits}"

candidates = phone_re.findall(sample)
valid = [n for n in (clean_and_validate(c) for c in candidates) if n]
print(valid)
# ['+4915712345678', '+491609876543']

Что происходит: сначала убираем код страны (0049 или 49), затем перепроверяем, что оставшийся префикс — оператор, и что длина укладывается в 10–12 цифр. Regex — фильтр первого приближения (см. теорию), окончательное решение принимает обычный код.

Пример 5. Границы слова в email: почему нельзя жадный класс символов

Задание 3 запрещает точку в начале и в конце локальной части email. \b сам по себе этого не гарантирует.

local = r"[a-zA-Z0-9_](?:[a-zA-Z0-9._]*[a-zA-Z0-9_])?"
email_re = re.compile(rf"\b{local}@[a-zA-Z0-9.-]+\.[a-zA-Z]{{2,3}}\b")
sample_emails = "ok: user.name@sub.domain.com, bad: .user@example.com, bad2: user.@example.com"
print(email_re.findall(sample_emails))
# ['user.name@sub.domain.com', 'user@example.com']

Что происходит: первый адрес корректный и найден целиком. Во втором случае (.user@example.com) граница \b стоит не перед точкой, а перед u — точка не входит в «слово», поэтому шаблон честно находит внутри строки корректный кусок user@example.com, игнорируя ведущую точку. А вот user.@example.com не совпал вовсе — локальная часть по построению не может заканчиваться точкой (см. разбор в «Ошибках»). Вывод тот же, что в примере 4: regex ловит форму, семантику иногда приходится доопределять шаблоном или кодом.

Пример 6. Номер карты: повторяющаяся группа (?:\d{4}[- ]?){3}\d{4}

Задание 4 — самое «геометрическое»: 16 цифр четвёрками, между которыми не более одного разделителя.

card_re = re.compile(r"\b(?:\d{4}[- ]?){3}\d{4}\b")
sample_cards = "1234 5678 9012 3456 and 1234--5678--9012--3456 and 1234567812345678"
print(card_re.findall(sample_cards))
# ['1234 5678 9012 3456', '1234567812345678']

Что происходит: группа (?:\d{4}[- ]?){3} повторяется ровно 3 раза, и после каждой четвёрки цифр допускает не более одного пробела или дефиса — квантификатор ? у [- ] разрешает максимум один разделитель. Последний блок \d{4} идёт без хвостового разделителя. Строка с двойным дефисом 1234--5678--... не подошла: после первого дефиса шаблон ждёт сразу цифру, а не второй дефис — это и есть проверка «без двойных разделителей» из условия задания.

Пример 7. JSON туда и обратно через pathlib

Задание 1 читает grades.json и пишет quarterly_report.json. Современный способ — без open()/close() вручную (см. урок 58 — Файлы JSON и datetime и урок 50 — Файловая система).

import json
from pathlib import Path

students = [
    {"name": "Alice", "subject": "Math", "grade": 90, "date": "12-03-2025"},
    {"name": "Alice", "subject": "Math", "grade": 70, "date": "20-04-2025"},
]
json_path = Path("students.json")
json_path.write_text(json.dumps(students, indent=2, ensure_ascii=False), encoding="utf-8")

loaded = json.loads(json_path.read_text(encoding="utf-8"))
print(loaded[0])
# {'name': 'Alice', 'subject': 'Math', 'grade': 90, 'date': '12-03-2025'}

Что происходит: write_text/read_text с явным encoding="utf-8" заменяют пару open(...)/close() — файл закрывается автоматически. Без явной кодировки на Windows можно получить UnicodeDecodeError (см. «Ошибки»). json.dumps/loads работают со строкой в памяти — удобно проверить формат перед записью на диск.

Пример 8. Дата → квартал: datetime.strptime и своя функция

Задание 1 требует ровно то разбиение на кварталы, что описано в условии, — не «логичное», а именно указанное.

from datetime import datetime

def get_quarter(date_str: str) -> str | None:
    month = datetime.strptime(date_str, "%d-%m-%Y").month
    if 1 <= month <= 3:
        return "Q1"
    if 4 <= month <= 5:
        return "Q2"
    if 9 <= month <= 12:
        return "Q3"
    return None  # летние месяцы пропускаем

for date_str in ("12-03-2025", "20-04-2025", "06-09-2025"):
    print(date_str, "->", get_quarter(date_str))
# 12-03-2025 -> Q1
# 20-04-2025 -> Q2
# 06-09-2025 -> Q3

Что происходит: strptime разбирает строку по явному формату "%d-%m-%Y" и возвращает объект datetime, у которого сразу есть .month. В исходном решении курса эти метки были перепутаны местами (январь–март помечены как Q2) — разбор в разделе «Ошибки».

Пример 9. Группировка без if: defaultdict(lambda: defaultdict(list))

Оценки нужно сгруппировать по ученику и кварталу одновременно — двухуровневый словарь без проверок «есть ли уже такой ключ» (см. урок 40 — Модуль collections).

from collections import defaultdict

entries = [
    {"name": "Alice", "grade": 90, "date": "12-03-2025"},
    {"name": "Alice", "grade": 70, "date": "20-04-2025"},
    {"name": "Alice", "grade": 80, "date": "06-09-2025"},
]

by_quarter: defaultdict[str, list[int]] = defaultdict(list)
for row in entries:
    quarter = get_quarter(row["date"])
    if quarter:
        by_quarter[quarter].append(row["grade"])

report = {q: round(sum(v) / len(v), 2) for q, v in by_quarter.items()}
print(report)
# {'Q1': 90.0, 'Q2': 70.0, 'Q3': 80.0}

Что происходит: defaultdict(list) создаёт пустой список сам при первом обращении к новому ключу — append не упадёт с KeyError. В настоящем задании 1 нужен ещё один уровень вложенности (по предмету), для этого используют фабрику defaultdict(lambda: defaultdict(list)) — она уже встречалась в справочнике. Средние значения считает генераторное выражение — без ручного накопления суммы и счётчика.

Пример 10. Генератор + regex: лениво читаем файл и сразу фильтруем

Финальная связка: генератор из урока 56 отдаёт строки файла по одной, а regex из примера 5 сразу отбирает нужное — не обязательно грузить весь файл в память, если он большой.

from pathlib import Path

def emails_from_file(path: Path):
    with path.open(encoding="utf-8") as file:
        for line in file:
            for match in email_re.finditer(line):
                yield match.group()

contacts = Path("contacts.txt")
contacts.write_text(
    "Alice - alice@example.com\nnot an email line\nBob - bob.work@company.org\n",
    encoding="utf-8",
)
print(list(emails_from_file(contacts)))
# ['alice@example.com', 'bob.work@company.org']

Что происходит: emails_from_file — функция с yield, она не читает файл целиком в память, а отдаёт совпадения по одной строке за раз. finditer возвращает итератор объектов Match; match.group() достаёт саму найденную строку. Эта связка «генератор построчного чтения + regex внутри» закрывает задания 2 и 3 в общем виде.

Что делать дальше

Переходите к заданиям — там те же приёмы нужно собрать в четыре полных решения. Если что-то не запустилось так, как в примерах, — сначала загляните в типичные ошибки, готовые решения — в разделе «Решения».

Как запустить в VS Code

  1. Создайте папку python-fundamentals-practice/lesson-61/.
  2. Создайте файлы данных (grades.json, german_numbers.txt) и скрипты для каждого задания.
  3. Откройте терминал в VS Code (Ctrl + `).
  4. Активируйте виртуальное окружение: venv\Scripts\activate (Windows) или source venv/bin/activate (Mac/Linux).
  5. Запустите: python task_01_grades.py