Введение: Почему «работает» — это не всегда «хорошо»
Каждый из нас хотя бы раз в жизни писал код, который можно описать фразой: «Ну, оно как-то работает, лучше не трогать». Мы наспех добавляем костыль, чтобы успеть к дедлайну, оставляем переменную с именем 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. Он гласит, что каждый класс или функция должны иметь только одну причину для изменения.
Представьте себе класс, который:
Загружает данные из базы данных.
Выполняет над ними сложные вычисления.
Форматирует результат в 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. Импорты
Импорты всегда должны быть в начале файла и сгруппированы в следующем порядке, с пустой строкой между группами:
Импорты из стандартной библиотеки Python (
os,sys,datetime).Импорты сторонних библиотек (
requests,sqlalchemy,pandas).Импорты из ваших собственных модулей проекта.
Плохо (все вперемешку):
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
Зачем это нужно?
Помощь в IDE: Современные редакторы кода подхватывают docstrings и показывают их в виде всплывающих подсказок, когда вы вызываете функцию.
Встроенная справка: Любой пользователь может получить эту информацию в интерактивной консоли с помощью функции
help(). Попробуйте:help(calculate_price_with_discount).Автоматическая генерация документации: Инструменты вроде 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)

Andrey_Solomatin
07.11.2025 09:02list(set(map(lambda name: name.upper(), filter(lambda name: name, names))))Этот код ужасен.
Но то, что вы предложили это далего не лучший подход, а реализация вообще с ошибкой.
Pythonic код будетlist({name.upper() for name in names if name})Дальше читать не стал, я не довeряю вашей компетенции после превого примера.
tenzink
В теории всё неплохо. Но часть иллюстрирующих примеров откровенно неудачные.
1. Вот это совсем не `просто и не очевидно`. В добавок к мусорной переменной
seen_namesещё и посадили баг. Потерялась сортированностьprocessed_names:Оригинальный код не эталон читабельности, но для программиста на python понятен, хотя с list comprehensions становится более идиоматичным
2. Вот это тоже очень криво для python
всю эту простыню лучше заменить чем-то более читабельным