Введение: Почему «работает» — это не всегда «хорошо»
Каждый из нас хотя бы раз в жизни писал код, который можно описать фразой: «Ну, оно как-то работает, лучше не трогать». Мы наспех добавляем костыль, чтобы успеть к дедлайну, оставляем переменную с именем 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-сообществе.
Уверен, у вас все получится. Вперед, к практике!
Комментарии (7)

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ряю вашей компетенции после превого примера.

AndreyAseev
07.11.2025 09:02Хорошее, полезное напоминание. Приятно было прочитать, если не брать во внимание достаточно вырожденные примеры.
Я бы ещё добавил:
Не забывать type hints там, где нет очевидной логики на уровне линтеров.
Не забывать про NamedTuples, dataclass для группировки предметных данных.
Обратить внимание на логику разделения модулей. Там есть нетривиальные ходы.
Про list comprehension ничего не сказано, а стоило бы.
На самом деле, все трюки и не перечислить в одной статье. Python даёт очень много мощных инструментов, преимущество которых не всегда лежит на поверхности.

qark
07.11.2025 09:02black, flake8, isort заменяются одним Ruff.

CrazyOpossum
07.11.2025 09:02У ruff фатальный недостаток. Не проверяет сам себя, не принимает патчи на питоне, тесты ruff и тесты корректности ruff - по разному написаны.

astentx
07.11.2025 09:02В 101 раз повторено одно и то же с упрощёнными примерами, а таким важным вещам, как типизация, уделена одна строчка между делом.
data.get("username")почему-то не попадает под критикуемую далее категорию магических констант. Хотя не раз в одном продукте виделuser_nameв одном месте иusernameв другом. И где тот интересный баланс между YAGNI и обкладыванием dataclass'ами всего подряд? Или выбор между NamedTuple и dataclass? Или вообще протоколом?Из интересного увидел только использование ветки
elseвtry: впервые встречаю среди прочитанного кода, и очень интересная рекомендация по использованию: вынести вtryтолько потенциально опасный кусок. А как же читаемость? Код друг за другом - все понятно, ожидаемые исключения в конце ветками - тоже понятно. А эта конструкция смотрится странно, даже интересно, что вдохновило дизайнеров языка на такое. Какая реальная польза отelseвместо описания happy-path и обработки разных исключений разом без детализации, кто же там споткнулся?
tenzink
В теории всё неплохо. Но часть иллюстрирующих примеров откровенно неудачные.
1. Вот это совсем не `просто и не очевидно`. В добавок к мусорной переменной
seen_namesещё и посадили баг. Потерялась сортированностьprocessed_names:Оригинальный код не эталон читабельности, но для программиста на python понятен, хотя с list comprehensions становится более идиоматичным
2. Вот это тоже очень криво для python
всю эту простыню лучше заменить чем-то более читабельным