Введение: Почему «работает» — это не всегда «хорошо»

Каждый из нас хотя бы раз в жизни писал код, который можно описать фразой: «Ну, оно как-то работает, лучше не трогать». Мы наспех добавляем костыль, чтобы успеть к дедлайну, оставляем переменную с именем data2 или пишем функцию на 200 строк, обещая себе вернуться к ней «позже». И знаете что? Это «позже» никогда не наступает.

Проблема в том, что в программировании «работает» — это лишь самый первый, базовый уровень качества. Корабль, у которого течь в корпусе, тоже ведь «плывет» — вопрос лишь в том, как долго и какой ценой. В мире разработки эта цена называется техническим долгом: чем больше хаоса в коде, тем дороже и медленнее становится каждая следующая доработка, каждое исправление бага. Проект, который поначалу летел на всех парах, начинает вязнуть в болоте собственной сложности.

В этой статье мы не будем говорить о мимолетных трендах. Мы разберем фундаментальные принципы и лучшие практики, которые превращают обычный работающий код в профессиональный, поддерживаемый и надежный продукт. Мы поговорим о том, как писать код, которым можно гордиться.

2. Фундаментальные принципы чистого кода

Прежде чем мы погрузимся в специфику Python, важно понять «большую четверку» принципов, которые лежат в основе чистого кода в любом языке. Это не строгие правила, а скорее компас, который помогает принимать верные архитектурные решения. Если вы усвоите эту философию, многие практические приемы станут для вас очевидными.

KISS (Keep It Simple, Stupid) — Будь проще

Этот принцип, родом из инженерной среды ВМС США, гласит: системы работают лучше всего, если они остаются простыми, а не усложняются. Программисты, особенно начинающие, любят демонстрировать свою эрудицию, создавая хитроумные однострочные решения или сложные абстракции. Но код пишется один раз, а читается — десятки. И через полгода даже вы сами не сможете быстро разобраться в собственном «гениальном» решении.

Плохо (сложно и непонятно):

# Фильтруем дубликаты, приводим к верхнему регистру и отбрасываем пустые строки
names = ["john", "jane", "", "john", "doe"]
processed_names = list(set(map(lambda name: name.upper(), filter(lambda name: name, names))))

Технически, это работает. Но сколько времени нужно, чтобы понять, что здесь происходит?

Хорошо (просто и очевидно):

names = ["john", "jane", "", "john", "doe"]
processed_names = []
seen_names = set()

for name in names:
    if name and name not in seen_names:
        processed_names.append(name.upper())
        seen_names.add(name)

Этот код длиннее, но его логика кристально ясна с первого взгляда. Он не требует умственного напряжения для понимания. В простоте — сила.

DRY (Don't Repeat Yourself) — Не повторяйся

Каждый фрагмент знания в системе должен иметь единственное, однозначное и авторитетное представление. Проще говоря: если вы скопировали и вставили кусок кода, вы, скорее всего, делаете что-то не так.

Повторение кода — это мина замедленного действия. Когда вам понадобится изменить логику, вам придется искать все ее копии и исправлять каждую из них. Забудете хотя бы одну — получите трудноуловимый баг.

Плохо (повторяющийся код):

def process_user_data(user_data):
    # ... какая-то логика ...
    username = user_data["username"]
    normalized_username = username.strip().lower()
    print(f"Processing user: {normalized_username}")
    # ... еще логика ...

def validate_user_data(user_data):
    # ... какая-то логика ...
    username = user_data["username"]
    normalized_username = username.strip().lower()
    if not normalized_username:
        return False
    # ... еще логика ...

Нормализация имени пользователя дублируется.

Хорошо (логика вынесена в функцию):

def get_normalized_username(user_data):
    """Извлекает и нормализует имя пользователя из данных."""
    username = user_data.get("username", "")
    return username.strip().lower()

def process_user_data(user_data):
    normalized_username = get_normalized_username(user_data)
    print(f"Processing user: {normalized_username}")
    # ... остальная логика ...

def validate_user_data(user_data):
    normalized_username = get_normalized_username(user_data)
    if not normalized_username:
        return False
    # ... остальная логика ...

Теперь, если правила нормализации изменятся (например, нужно будет убирать спецсимволы), вам придется поправить код только в одном месте.

YAGNI (You Ain't Gonna Need It) — Тебе это не понадобится

Это принцип борьбы с избыточной сложностью и преждевременной оптимизацией. Очень часто мы пытаемся предугадать будущее и пишем код, который может быть полезен когда-нибудь потом. В результате система обрастает мертвым грузом — функциями и классами, которые никто никогда не использует, но которые нужно поддерживать.

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

Принцип единственной ответственности (Single Responsibility Principle)

Этот принцип — первый и самый важный из набора принципов SOLID. Он гласит, что каждый класс или функция должны иметь только одну причину для изменения.

Представьте себе класс, который:

  1. Загружает данные из базы данных.

  2. Выполняет над ними сложные вычисления.

  3. Форматирует результат в HTML.

У этого класса есть три причины для изменения: поменялась схема БД, изменился алгоритм вычислений или поменялся дизайн HTML-отчета. Это «швейцарский нож», который делает все, но все делает посредственно и его сложно тестировать и поддерживать.

Правильный подход — разделить эту логику на три разных класса:

  • UserRepository — отвечает только за взаимодействие с БД.

  • DataCalculator — отвечает только за вычисления.

  • ReportGenerator — отвечает только за создание HTML.

Такой код становится модульным, тестируемым и гораздо более понятным. Каждая часть занимается своим делом.

3. Правила хорошего тона в Python: Следуем PEP 8

Если фундаментальные принципы — это философия чистого кода, то PEP 8 — это его грамматика и пунктуация. Это официальное руководство по стилю кода Python, и его цель проста до гениальности: обеспечить единообразие и читаемость кода, написанного разными разработчиками.

Соблюдение PEP 8 — это не слепое следование догмам. Это знак уважения к сообществу, к вашим коллегам и к самому себе в будущем. Как сказал Гвидо ван Россум, создатель Python: «Код читают гораздо чаще, чем пишут». PEP 8 делает процесс чтения максимально комфортным.

Вам не нужно учить его наизусть. Достаточно понять ключевые идеи и делегировать контроль инструментам, о которых мы поговорим в конце.

Ключевые моменты PEP 8

Вот несколько самых важных правил, которые мгновенно улучшат ваш код.

1. Отступы

4 пробела на каждый уровень вложенности. Точка. Споры о пробелах и табах в мире Python давно закончены. Настройте свой редактор кода так, чтобы клавиша Tab автоматически вставляла 4 пробела.

2. Длина строки

Старайтесь ограничивать длину строки 79-99 символами. Классический стандарт — 79, но современные инструменты, такие как black, используют 88.

Зачем? Это позволяет комфортно работать с несколькими файлами на одном экране и упрощает просмотр изменений (diff) в системах контроля версий.

Плохо (трудно читать):

from my_module import a_very_long_function_name, another_super_long_function_name, yet_another_long_name

Хорошо (используем переносы внутри скобок):

from my_module import (
    a_very_long_function_name,
    another_super_long_function_name,
    yet_another_long_name,
)

3. Импорты

Импорты всегда должны быть в начале файла и сгруппированы в следующем порядке, с пустой строкой между группами:

  1. Импорты из стандартной библиотеки Python (os, sys, datetime).

  2. Импорты сторонних библиотек (requests, sqlalchemy, pandas).

  3. Импорты из ваших собственных модулей проекта.

Плохо (все вперемешку):

import requests
from my_project.utils import helper_function
import os
from sqlalchemy import create_engine
import sys

Хорошо (сгруппировано и отсортировано):

import os
import sys

import requests
from sqlalchemy import create_engine

from my_project.utils import helper_function

Такая структура сразу дает понять, какие зависимости есть у модуля.

4. Пробелы и пустые строки

  • Используйте пробелы вокруг операторов: x = y + 1, а не x=y+1.

  • Не ставьте пробелы сразу после открывающей и перед закрывающей скобкой: my_func(arg1, arg2), а не my_func( arg1, arg2 ).

  • Используйте пустые строки для разделения логических блоков. Функция не должна быть монолитным полотном кода. Думайте о пустых строках как о параграфах в тексте — они помогают структурировать мысль.

Плохо (визуальная "каша"):

def process_data(data):
    user_id=data['id']
    print(f"Starting process for user {user_id}")
    result=heavy_calculation(data)
    if result.is_valid:
        save_to_db(result)
    return True

Хорошо (логические блоки разделены):

def process_data(data):
    user_id = data['id']
    print(f"Starting process for user {user_id}")

    result = heavy_calculation(data)

    if result.is_valid:
        save_to_db(result)

    return True

Автоматизация — ваш лучший друг

Заучивать все правила PEP 8 — контрпродуктивно. Гораздо эффективнее использовать инструменты, которые сделают всю грязную работу за вас.

  • flake8: Это линтер, который просканирует ваш код и укажет на все несоответствия PEP 8, а также на возможные логические ошибки.

  • black: Это бескомпромиссный форматер кода. Он не спорит, а просто переформатирует ваш код в едином, каноническом стиле. Его девиз: «С black спорить бесполезно». Внедрив его в команде, вы навсегда избавитесь от споров о стиле.

  • isort: Специализированный инструмент, который автоматически сортирует ваши импорты в правильном порядке.

Интегрируйте эти инструменты в свою IDE или CI/CD пайплайны, и ваш код всегда будет соответствовать стандарту без малейших усилий. Соблюдение PEP 8 — это не про ограничения, а про дисциплину. Это признак профессионализма и уважения к коллегам.

4. Именование: Искусство давать правильные имена

Если бы у программиста была только одна суперспособность, это было бы умение давать точные и ясные имена. Звучит просто? На практике это один из самых сложных и важных навыков. Хорошее имя превращает загадочный код в понятную историю. Плохое — заставляет часами разгадывать ребусы, написанные вами же пару недель назад.

Главная цель именования — сделать код самодокументируемым. В идеальном мире ваш код должен быть настолько понятен, что комментарии, объясняющие, что делает переменная или функция, становятся просто не нужны.

Говорящие имена: Код, который читается как проза

Переменная, функция или класс должны своим названием отвечать на три вопроса: зачем она существует, что она делает и как используется.

Плохо (загадочно):

def process_data(d):
    l = []
    for i in d:
        if i[1] > 10:
            l.append(i[0])
    return l

Что такое d? Что такое l? Что значат i[0] и i[1]? Чтобы понять этот код, нужно его "декодировать".

Лучше (уже понятнее):

def get_passed_students(student_records):
    approved_students = []
    for record in student_records:
        grade = record[1]
        name = record[0]
        if grade > 10:
            approved_students.append(name)
    return approved_students

Этот код уже не требует расшифровки. Мы понимаем, с какими данными работаем. Но можно еще лучше. Если student_records — это список кортежей, лучше использовать именованный кортеж или dataclass, чтобы избавит��ся от "магических индексов" [0] и [1].

Соглашения по именованию в Python

Сообщество Python уже договорилось о едином стиле, чтобы нам не пришлось изобретать велосипед. Эти правила — часть PEP 8:

  • snake_case: для переменных и функций. Слова разделяются нижним подчеркиванием (user_name, calculate_total_price).

  • CamelCase (или PascalCase): для классов (User, HttpRequest, PaymentProcessor).

  • UPPER_SNAKE_CASE: для констант (MAX_CONNECTIONS, DEFAULT_TIMEOUT).

Придерживаясь этих правил, вы делаете код предсказуемым для любого Python-разработчика.

Избегайте "магических чисел" и строк

«Магическое число» — это числовое значение, которое появляется в коде без объяснения. Оно работает, но никто не знает, почему именно оно.

Плохо:

# Что означает 42? Статус "отправлено"? Код ошибки?
if order_status == 42:
    send_order_to_warehouse(order)

Хорошо (используем именованную константу):

STATUS_READY_FOR_DISPATCH = 42

...

if order_status == STATUS_READY_FOR_DISPATCH:
    send_order_to_warehouse(order)

Теперь код говорит сам за себя. Если значение статуса изменится, вам нужно будет поправить его только в одном месте, а не искать все вхождения числа 42 по всему проекту. То же самое касается и строк.

Примеры «до» и «после»

Давайте посмотрим, как простое переименование преображает код.

До (непонятно):

def fn(c, t):
    p = c * (1 - t)
    return p

После (очевидно):

def calculate_price_with_discount(full_price: float, discount_rate: float) -> float:
    """Рассчитывает итоговую цену товара с учетом скидки."""
    final_price = full_price * (1 - discount_rate)
    return final_price

Обратите внимание, как добавление аннотаций типов (: float) и докстринга еще больше улучшает читаемость.

Именование — это не формальность, а ключевой навык, который напрямую влияет на поддерживаемость проекта. Потратьте лишние 10 секунд на то, чтобы придумать хорошее имя. Ваше будущее "я" и ваши коллеги скажут вам за это спасибо.

5. Функции и методы: Делаем их маленькими и сфокусированными

Представьте, что ваша функция — это инструмент в ящике мастера. Хороший мастер имеет набор специализированных инструментов: отдельный ключ для каждой гайки, отдельную отвертку для каждого винта. Он не пытается закрутить все одним гигантским «швейцарским ножом». В программировании точно так же: функции — это наши инструменты, и чем они проще и сфокусированнее, тем мощнее и надежнее вся система.

Функции — это базовые строительные блоки любой программы. Если эти блоки — громоздкие, запутанные и хрупкие монолиты, то все здание вашей программы будет таким же.

Функции должны быть короткими

Это самое главное правило. Насколько короткими? В идеале — не больше одного экрана. Если вам нужно скроллить, чтобы увидеть всю функцию целиком, она почти наверняка слишком длинная.

Длинные функции — это симптом того, что они делают слишком много. Их сложно понять, сложно тестировать и очень легко сломать, внося изменения.

Плохо (функция-монстр):

def register_new_user(request_data):
    # 1. Валидация данных
    email = request_data.get("email")
    password = request_data.get("password")
    if not email or "@" not in email:
        raise ValueError("Invalid email provided")
    if not password or len(password) < 8:
        raise ValueError("Password is too short")

    # 2. Проверка, не занят ли email
    if User.objects.filter(email=email).exists():
        raise ValueError("This email is already taken")

    # 3. Создание пользователя в базе данных
    hashed_password = hash_password(password)
    new_user = User.objects.create(email=email, password=hashed_password)
    
    # 4. Отправка приветственного письма
    send_mail(
        "Welcome to our platform!",
        f"Hello, {email}! Thank you for registering.",
        "noreply@example.com",
        [email],
        fail_silently=False,
    )

    return new_user

Эта функция делает как минимум четыре разные вещи. Она нарушает все принципы, о которых мы говорили.

Одна функция — одна задача

Это прямое следствие предыдущего пункта и применение Принципа единственной ответственности на уровне функций. Каждая функция должна выполнять только одну логическую операцию и делать это хорошо.

Как понять, что функция делает только одну вещь? Попробуйте описать, что она делает, в одном коротком предложении. Если в описании появляется слово «и» — это верный признак, что функцию пора разделить.

Давайте отрефакторим наш пример:

Хорошо (декомпозиция на маленькие функции):

def validate_registration_data(data):
    """Проверяет корректность данных для регистрации. Вызывает ValueError в случае ошибки."""
    email = data.get("email")
    if not email or "@" not in email:
        raise ValueError("Invalid email provided")
    
    password = data.get("password")
    if not password or len(password) < 8:
        raise ValueError("Password is too short")

def create_user_in_database(email, password):
    """Создает нового пользователя в БД. Вызывает ValueError, если email занят."""
    if User.objects.filter(email=email).exists():
        raise ValueError("This email is already taken")
    
    hashed_password = hash_password(password)
    return User.objects.create(email=email, password=hashed_password)

def send_welcome_email(user_email):
    """Отправляет приветственное письмо новому пользователю."""
    send_mail(
        "Welcome to our platform!",
        f"Hello, {user_email}! Thank you for registering.",
        "noreply@example.com",
        [user_email],
    )

def register_new_user(request_data):
    """Основная функция-координатор процесса регистрации."""
    validate_registration_data(request_data)
    
    user = create_user_in_database(
        email=request_data["email"],
        password=request_data["password"]
    )
    
    send_welcome_email(user.email)
    
    return user

Смотрите, что произошло. У нас появилась главная функция register_new_user, которая теперь читается как рассказ. Она не выполняет работу сама, а координирует вызов других, маленьких и сфокусированных функций. Каждую из этих маленьких функций теперь легко понять, протестировать и использовать повторно в других частях системы.

Меньше аргументов

Большое количество аргум��нтов у функции (скажем, больше трех) — это «код с запашком». Это часто говорит о том, что функция пытается сделать слишком много. Кроме того, длинный список аргументов легко перепутать при вызове.

Плохо:

def create_product(name, description, price, weight, category, supplier_id):
    # ...

Хорошо (группируем аргументы в объект):

from dataclasses import dataclass

@dataclass
class ProductData:
    name: str
    description: str
    price: float
    weight: float
    category: str
    supplier_id: int

def create_product(product_data: ProductData):
    # ...

Теперь у функции один, понятный аргумент, который инкапсулирует все данные о продукте.

Избегайте побочных эффектов (side effects)

Лучшая функция — это та, которая берет данные на вход, выполняет вычисления и возвращает результат, не меняя ничего за своими пределами. Функция, которая изменяет глобальную переменную или модифицирует один из своих входных аргументов, имеет побочные эффекты. Такой код непредсказуем и его сложно отлаживать.

Плохо (изменяет входной список):

def add_admin_user(users_list):
    users_list.append({"name": "admin", "role": "admin"})

users = [{"name": "john", "role": "user"}]
add_admin_user(users) # Теперь список `users` изменен

Хорошо (возвращает новый список):

def add_admin_user(users_list):
    """Возвращает новый список с добавленным админом."""
    return users_list + [{"name": "admin", "role": "admin"}]

users = [{"name": "john", "role": "user"}]
new_users = add_admin_user(users) # `users` остался неизменным

Такой подход делает поток данных в программе явным и предсказуемым. Вы всегда знаете, откуда берутся изменения.

6. Комментарии и документирование: Когда и как

Существует известное изречение: «Хороший код комментирует сам себя». В этом есть огромная доля правды. Если вы следовали предыдущим советам — давали понятные имена, писали маленькие, сфокусированные функции — потребность в комментариях, объясняющих, что делает ваш код, отпадает сама собой.

Комментарии — это не дезодорант для кода с душком. Нельзя написать запутанную функцию и «исправить» ситуацию, залепив ее сверху полотном объяснений. Такой подход только усугубляет проблему, потому что комментарии имеют свойство устаревать. Код меняется, а комментарии — нет, и в итоге они начинают лгать, внося еще больше путаницы.

Тем не менее, это не значит, что комментарии — это абсолютное зло. У них есть свои, очень важные, но узкоспециализированные задачи.

«Почему», а не «Что»

Самое главное правило: комментарии должны объяснять не что делает код, а почему он это делает именно так.

Если ваш код настолько сложен, что требует пояснения, что он делает, ваш первый шаг — попытаться его переписать и упростить. Если же код прост, но решение, которое он реализует, неочевидно, — вот идеальное место для комментария.

Плохо (комментарий-капитан Очевидность):

# Увеличиваем счетчик на единицу
i += 1

Этот комментарий просто создает визуальный шум. Он не несет никакой полезной информации.

Хорошо (объяснение неочевидного решения):

# Мы используем побитовый сдвиг вместо деления на 2,
# так как этот эндпоинт вызывается 1000 раз в секунду,
# и эта микрооптимизация дает прирост в 5% производительности.
value = count >> 1

Здесь комментарий бесценен. Он объясняет бизнес-контекст и причину, по которой был выбран не самый очевидный путь. Без него следующий разработчик (или вы сами через полгода) мог бы «улучшить» этот код, убрав «странный» сдвиг и тем самым вызвав деградацию производительности.

Docstrings: Официальная документация вашего кода

Если inline-комментарии — это заметки на полях, то docstrings (строки документации) — это официальный паспорт вашей функции, класса или модуля. Это стандартный способ документирования публичного API в Python.

Они заключаются в тройные кавычки ("""...""" или '''...''') и располагаются сразу после объявления.

def calculate_price_with_discount(full_price: float, discount_rate: float) -> float:
    """Рассчитывает итоговую цену товара с учетом скидки.

    Args:
        full_price: Полная цена товара (должна быть > 0).
        discount_rate: Коэффициент скидки (от 0.0 до 1.0).

    Returns:
        Итоговая цена после применения скидки.

    Raises:
        ValueError: Если цена или скидка выходят за допустимые пределы.
    """
    if not 0 < full_price:
        raise ValueError("Price must be positive.")
    if not 0.0 <= discount_rate <= 1.0:
        raise ValueError("Discount rate must be between 0 and 1.")

    final_price = full_price * (1 - discount_rate)
    return final_price

Зачем это нужно?

  1. Помощь в IDE: Современные редакторы кода подхватывают docstrings и показывают их в виде всплывающих подсказок, когда вы вызываете функцию.

  2. Встроенная справка: Любой пользователь может получить эту информацию в интерактивной консоли с помощью функции help(). Попробуйте: help(calculate_price_with_discount).

  3. Автоматическая генерация документации: Инструменты вроде Sphinx могут просканировать ваш код и собрать из docstrings полноценную HTML-документацию для вашего проекта.

TODO, FIXME: Заметки на будущее

Иногда нужно оставить в коде пометку для себя или коллег. Для этого есть стандартные маркеры:

  • # TODO: — используется для обозначения места, где требуется доработка или реализация новой функциональности. Это напоминание, что работа здесь еще не закончена.

  • # FIXME: — используется для обозначения куска кода, который заведомо работает неправильно или неоптимально и требует исправления.

# TODO: Добавить поддержку разных валют, когда появится API от банка.
def get_currency_rate(currency="USD"):
    if currency == "USD":
        return 1.0
    # FIXME: Возвращает некорректный курс для EUR, временно захардкожено.
    if currency == "EUR":
        return 0.95 
    raise NotImplementedError("Currency not supported yet.")

Многие IDE подсвечивают такие маркеры, и их легко найти по всему проекту. Главное — не позволяйте им жить в коде вечно. TODO и FIXME должны быть временными и в идеале дублироваться задачами в вашем таск-трекере.

В заключение: относитесь к комментариям как к сильному, но опасному инструменту. Ваш главный приоритет — писать ясный код. А комментарии и docstrings используйте тогда, когда код не может рассказать всю историю сам.

7. Обработка ошибок и исключений

Ошибки — это не провал, это неизбежная часть жизни любой программы. Сеть может отвалиться, файл — не найтись, а пользователь — ввести строку вместо числа. Признак зрелого и профессионального кода — не в том, что он никогда не падает, а в том, как он предсказуемо и элегантно ведет себя в нештатных ситуациях.

Неправильная обработка исключений — это бомба замедленного действия. Она может приводить к скрытым багам, потере данных и часам мучительной отладки. К счастью, в Python есть мощные и ясные инструменты для работы с ошибками.

Не подавляйте исключения молча: Кардинальный грех except: pass

Это худшее, что вы можете сделать в своем коде. Конструкция try...except: pass — это чёрная дыра, которая молча проглатывает абсолютно все ошибки.

Ужасно (никогда так не делайте):

import json

raw_config = '{"port": 8000, "host": "localhost"' # <-- Ошибка: нет закрывающей скобки

config = {}
try:
    config = json.loads(raw_config)
except:
    pass # Ошибка проигнорирована!

# Программа продолжает работать с пустым `config`,
# и упадет где-то гораздо позже, без намека на первопричину.
host = config.get("host") # host будет None
connect_to_database(host) # Истинная проблема скрыта

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

Правильно (как минимум, залогируйте ошибку):

import logging

try:
    config = json.loads(raw_config)
except json.JSONDecodeError as e:
    logging.error(f"Failed to decode config: {e}")
    # Предпринять действия: использовать конфиг по умолчанию, или завершить работу
    raise SystemExit("Configuration is broken. Shutting down.")

Теперь ошибка не просто замечена, но и зарегистрирована, а программа принимает осмысленное решение о том, что делать дальше.

Ловите конкретные исключения

Не стоит ловить базовый Exception, если вы можете предсказать конкретную ошибку. Когда вы лов��те Exception, вы рискуете перехватить то, чего не ожидали, например, KeyboardInterrupt (когда пользователь нажимает Ctrl+C) или SystemExit.

Плохо (слишком широкая ловушка):

try:
    process_file("data.csv")
except Exception as e:
    print(f"An error occurred: {e}")

Что это за ошибка? Файл не найден? Нет прав на чтение? Ошибка в самом process_file? Неизвестно.

Хорошо (конкретика и разделение логики):

try:
    process_file("data.csv")
except FileNotFoundError:
    logging.error("Config file 'data.csv' not found.")
except PermissionError:
    logging.error("Not enough permissions to read 'data.csv'.")
except Exception:
    logging.exception("An unexpected error occurred during file processing.")
    # `logging.exception` также запишет полный traceback ошибки

Такой код не только обрабатывает ожидаемые проблемы по-разному, но и оставляет "страховочный" блок для действительно непредвиденных ситуаций.

Используйте try-except-else-finally

Эта полная конструкция — мощный инструмент для структурирования кода.

  • try: Блок, где вы ожидаете возникновение ошибки. Старайтесь делать его как можно меньше.

  • except: Выполняется, если в блоке try произошло исключение.

  • else: Выполняется, если в блоке try исключений не было. Это идеальное место для «кода счастливого пути» (happy path), который должен выполниться только после успешного завершения рискованной операции.

  • finally: Выполняется всегда, независимо от того, было исключение или нет. Идеально подходит для очистки ресурсов: закрытия файлов, сетевых соединений или транзакций в базе данных.

Пример из жизни:

f = None
try:
    f = open("my_file.txt", "r")
except FileNotFoundError:
    print("File not found, nothing to process.")
else:
    # Этот код выполнится только если файл успешно открылся
    print("File opened successfully. Processing content...")
    content = f.read()
    # ... какая-то работа с content ...
finally:
    # Этот код выполнится в любом случае
    if f:
        print("Closing file.")
        f.close()

Использование else позволяет минимизировать код внутри try. В try мы оставляем только одну рискованную операцию (open), а всю остальную логику выносим в else.

8. Практикум: Проверьте себя

Теория без практики мертва. Давайте проверим, как вы усвоили принципы чистого кода. Ниже — 5 задач. Попробуйте мысленно или в редакторе решить каждую, прежде чем открывать спойлер с решением.

Задача 1: Искусство именования и простоты

Показать условие и решение

Условие:
Этот код отбирает пользователей, которые соответствуют определенным критериям. Сможете ли вы сделать его читаемым без единого комментария?

Код для рефакторинга:

# Исходные данные: список кортежей (имя, возраст, активен ли)
d = [('Alice', 25, True), ('Bob', 17, True), ('Charlie', 30, False), ('David', 40, True)]

def a(some_list):
    r = []
    for i in some_list:
        # Проверяем, что пользователь активен и старше 18
        if i[2] and i[1] > 18:
            r.append(i[0])
    return r

print(a(d))

Решение и объяснение:

Улучшенный код:

from typing import List, Tuple

UserData = Tuple[str, int, bool]

users_data: List[UserData] = [
    ('Alice', 25, True),
    ('Bob', 17, True),
    ('Charlie', 30, False),
    ('David', 40, True)
]

def get_active_adult_users(users: List[UserData]) -> List[str]:
    """Возвращает имена совершеннолетних и активных пользователей."""
    active_adults = []
    for name, age, is_active in users:
        if is_active and age > 18:
            active_adults.append(name)
    return active_adults

print(get_active_adult_users(users_data))

Что было сделано:

  • Говорящие имена: d, a, r, i заменены на users_data, get_active_adult_users, active_adults и name, age, is_active. Теперь код читается как обычный текст.

  • Распаковка кортежей: Вместо «магических индексов» i[0], i[1], i[2] используется распаковка for name, age, is_active in users:. Это гораздо нагляднее.

  • Аннотации типов: Добавлены typing для улучшения читаемости и помощи статическим анализаторам.

Задача 2: Принцип единственной ответственности (SRP)

Показать условие и решение

Условие:
Эта функция делает сразу три вещи: парсит строку, валидирует данные и форматирует их для вывода. Разделите ее на несколько сфокусированных функций.

Код для рефакторинга:

def process_user_string(raw_data: str):
    # 1. Парсинг
    try:
        parts = raw_data.split(',')
        name_part = parts[0].split('=')[1]
        age_part = int(parts[1].split('=')[1])
    except (IndexError, ValueError):
        return "Error: Invalid data format"

    # 2. Валидация
    if age_part < 0:
        return "Error: Invalid age"

    # 3. Форматирование
    return f"User: {name_part}, Age: {age_part}"

print(process_user_string("name=John,age=35"))

Решение и объяснение:

Улучшенный код:

def parse_user_data(raw_data: str) -> dict:
    """Парсит строку и возвращает словарь с данными пользователя."""
    parts = raw_data.split(',')
    name = parts[0].split('=')[1]
    age = int(parts[1].split('=')[1])
    return {"name": name, "age": age}

def validate_user_data(user_data: dict):
    """Валидирует данные пользователя. Вызывает ValueError в случае ошибки."""
    if user_data["age"] < 0:
        raise ValueError("Age cannot be negative.")

def format_user_info(user_data: dict) -> str:
    """Форматирует данные пользователя в строку для отображения."""
    return f"User: {user_data['name']}, Age: {user_data['age']}"

def process_user_string(raw_data: str) -> str:
    """Координирует процесс обработки данных пользователя."""
    try:
        user_data = parse_user_data(raw_data)
        validate_user_data(user_data)
        return format_user_info(user_data)
    except (ValueError, IndexError) as e:
        return f"Error: {e}"

print(process_user_string("name=John,age=35"))

Что было сделано:

  • Декомпозиция: Одна большая функция разделена на три маленькие (parse, validate, format), каждая из которых делает ровно одну вещь.

  • Явная обработка ошибок: Вместо возврата строки с ошибкой, валидатор теперь вызывает исключение ValueError. Это более правильный способ сигнализировать об ошибке.

  • Функция-координатор: Основная функция process_user_string теперь не выполняет логику сама, а управляет вызовом других функций, что делает ее простой для понимания.

Задача 3: Не повторяйся (DRY)

Показать условие и решение

Условие:
В этом коде есть два почти идентичных блока для расчета скидки. Как это можно исправить?

Код для рефакторинга:

def calculate_final_price_for_vip(price, quantity):
    total = price * quantity
    discount = total * 0.2  # Скидка 20% для VIP
    final_price = total - discount
    print(f"VIP customer final price: ${final_price:.2f}")
    return final_price

def calculate_final_price_for_regular(price, quantity):
    total = price * quantity
    discount = total * 0.05  # Скидка 5% для обычных клиентов
    final_price = total - discount
    print(f"Regular customer final price: ${final_price:.2f}")
    return final_price

Решение и объяснение:

Улучшенный код:

def calculate_final_price(price: float, quantity: int, discount_rate: float) -> float:
    """Рассчитывает итоговую цену на основе базовой цены, количества и скидки."""
    total = price * quantity
    discount = total * discount_rate
    return total - discount

# Код, который использует эту функцию
vip_price = calculate_final_price(100, 2, 0.20)
print(f"VIP customer final price: ${vip_price:.2f}")

regular_price = calculate_final_price(100, 2, 0.05)
print(f"Regular customer final price: ${regular_price:.2f}")

Что было сделано:

  • Устранение дублирования: Вместо двух почти одинаковых функций мы создали одну универсальную calculate_final_price.

  • Параметризация: Отличающаяся часть логики (процент скидки) была вынесена в параметр discount_rate.

  • Разделение ответственности: Теперь функция calculate_final_price отвечает только за расчет, а логика вывода результата (print) находится снаружи.

Задача 4: Надежная обработка ошибок

Показать условие и решение

Условие:
Эта функция пытается прочитать файл и обработать его содержимое, но обработка ошибок очень хрупкая. Сделайте ее надежной.

Код для рефакторинга:

def get_value_from_file(filepath):
    try:
        f = open(filepath, 'r')
        line = f.readline()
        value = int(line)
        print(f"Read value: {value}")
        f.close()
    except:
        print("Something went wrong.")

get_value_from_file("my_data.txt")

Решение и объяснение:

Улучшенный код:

def get_value_from_file(filepath: str):
    """
    Безопасно читает число из первой с��роки файла.
    """
    try:
        with open(filepath, 'r') as f:
            line = f.readline()
            value = int(line.strip())
    except FileNotFoundError:
        print(f"Error: File '{filepath}' not found.")
    except ValueError:
        print(f"Error: Could not convert content to integer in file '{filepath}'.")
    except Exception as e:
        print(f"An unexpected error occurred: {e}")
    else:
        # Блок 'else' выполняется, если ошибок не было
        print(f"Read value: {value}")

Что было сделано:

  • Использование with: Контекстный менеджер with open(...) автоматически и гарантированно закрывает файл, даже если внутри блока произойдет ошибка. Это предпочтительнее try...finally для работы с файлами.

  • Конкретные исключения: Вместо голого except мы ловим конкретные ошибки: FileNotFoundError и ValueError. Это позволяет нам давать пользователю более осмысленные сообщения об ошибках.

  • "Страховочный" except: Добавлен except Exception as e для отлова любых других, непредвиденных ошибок.

  • Использование else: Код, который должен выполняться только в случае успеха (happy path), вынесен в блок else, что делает блок try меньше и чище.

Задача 5: Чистота стиля (PEP 8)

Показать условие и решение

Условие:
Этот код работает, но от его форматирования болят глаза. Приведите его в соответствие со стандартами PEP 8.

Код для рефакторинга:

import sys, os

class user:
    def __init__(self, name,EMAIL):
        self.name=name
        self.email=EMAIL

def GET_USER_LIST( data_list ):
    userList=[]
    for item in data_list:
        if 'name' in item and 'email' in item:
            userList.append(user(item['name'], item['email']))
    return userList

Решение и объяснение:

Улучшенный код:

import os
import sys


class User:
    def __init__(self, name: str, email: str):
        self.name = name
        self.email = email


def get_user_list(data_list: list) -> list:
    """Создает список объектов User из списка словарей."""
    user_list = []
    for item in data_list:
        if 'name' in item and 'email' in item:
            user_list.append(User(item['name'], item['email']))
    return user_list

Что было сделано:

  • Импорты: Каждый импорт на новой строке, отсортированы (сначала стандартная библиотека).

  • Именование: Имя класса user исправлено на User (CamelCase). Имя функции GET_USER_LIST и переменной EMAIL исправлены на get_user_list и email (snake_case).

  • Пробелы: Добавлены пробелы вокруг операторов (=) и после запятых. Убраны лишние пробелы внутри скобок.

  • Пустые строки: Класс и функция отделены двумя пустыми строками для лучшей читаемости.

  • (Бонус) Аннотации и Docstrings: Добавлены аннотации типов и строка документации, чтобы сделать код еще более понятным.

Анонс новых статей, полезные материалы, а так же если в процессе решения возникнут сложности, обсудить их или задать вопрос по статье можно в моём Telegram-сообществе.

Уверен, у вас все получится. Вперед, к практике!

Комментарии (3)


  1. tenzink
    07.11.2025 09:02

    В теории всё неплохо. Но часть иллюстрирующих примеров откровенно неудачные.

    1. Вот это совсем не `просто и не очевидно`. В добавок к мусорной переменной seen_namesещё и посадили баг. Потерялась сортированность processed_names:

    names = ["john", "jane", "", "john", "doe"]
    processed_names = []
    seen_names = set()
    
    for name in names:
        if name and name not in seen_names:
            processed_names.append(name.upper())
            seen_names.add(name)
    

    Оригинальный код не эталон читабельности, но для программиста на python понятен, хотя с list comprehensions становится более идиоматичным

    processed_names = list(set(name.upper() for name in names if name))

    2. Вот это тоже очень криво для python

    def get_passed_students(student_records):
        approved_students = []
        for record in student_records:
            grade = record[1]
            name = record[0]
            if grade > 10:
                approved_students.append(name)
        return approved_students

    всю эту простыню лучше заменить чем-то более читабельным

    def get_passed_students_names(student_records):
        return [name for (name, grade) in student_records if grade > 10]


  1. laminar
    07.11.2025 09:02

    Статья для тех, кто книгу не читал?


  1. Andrey_Solomatin
    07.11.2025 09:02

    list(set(map(lambda name: name.upper(), filter(lambda name: name, names))))

    Этот код ужасен.

    Но то, что вы предложили это далего не лучший подход, а реализация вообще с ошибкой.

    Pythonic код будет

    list({name.upper() for name in names if name})

    Дальше читать не стал, я не довeряю вашей компетенции после превого примера.