40. Написание чистого и читаемого кода
На протяжении этой книги вы изучили синтаксис Python, структуры данных, управляющие конструкции, функции, классы и многие другие концепции программирования. Теперь вы можете писать программы, которые работают. Но есть ключевое различие между кодом, который работает, и кодом, который легко поддерживать — кодом, который вы и другие сможете понять, изменять и отлаживать через месяцы или годы.
Эта глава посвящена написанию чистого, читаемого кода. Вы изучите соглашения и практики, которые делают код на Python профессиональным и поддерживаемым. Это не просто произвольные правила — это проверенные в бою рекомендации, которые упрощают совместную работу, снижают количество ошибок и помогают вам понимать собственный код, когда вы возвращаетесь к нему позже.
40.1) Почему стиль важен: чтение кода против написания кода
40.1.1) Код читают чаще, чем пишут
Когда вы пишете код, вы тратите минуты или часы на его создание. Но этот код будет прочитан много раз: когда вы будете его отлаживать, когда добавите новые возможности, когда другие разработчики будут работать с ним и когда вы вернётесь к нему через месяцы, пытаясь вспомнить, что он делает.
Рассмотрим рабочий, но плохо оформленный код:
# ПРЕДУПРЕЖДЕНИЕ: Плохой стиль — только для демонстрации
def c(l):
t=0
for i in l:
t=t+i
return t/len(l)
data=[85,92,78,90,88]
result=c(data)
print(result) # Output: 86.6Этот код работает идеально. Он вычисляет среднее значение списка(list) чисел. Но чтобы понять, что именно он делает, требуется внимательный анализ. Теперь сравните его с этой версией:
def calculate_average(numbers):
"""Calculate the arithmetic mean of a list of numbers."""
total = 0
for number in numbers:
total = total + number
return total / len(numbers)
test_scores = [85, 92, 78, 90, 88]
average_score = calculate_average(test_scores)
print(average_score) # Output: 86.6Что делает вторую версию лучше?
- Имя функции (
calculate_average) ясно указывает на назначение - Имена переменных (
numbers,total,test_scores) описательные - Docstring объясняет, что делает функция
- Правильные пробелы делают структуру понятной
- Любой может понять этот код без изучения его в деталях
Обе версии дают одинаковый результат, но вторую версию понять можно сразу.
Ключевая мысль: код вы пишете один раз, но читаете его десятки или сотни раз. Потратив несколько лишних секунд на понятные имена и форматирование, вы сэкономите часы путаницы потом.
40.1.2) Читаемость снижает количество ошибок
Понятный код легче отлаживать, потому что вы быстро понимаете, что делает каждая часть. Когда имена переменных описательные, а структура аккуратная, логические ошибки заметить проще.
# Трудно отлаживать — что обозначают эти переменные?
# ПРЕДУПРЕЖДЕНИЕ: Плохой стиль — только для демонстрации
def process(x, y):
if x > y:
return x * (1 - y)
return x
result = process(100, 0.1)# Легко отлаживать — сразу понятно, что происходит
def apply_discount(price, discount_rate):
"""Calculate price after applying discount rate (0.0 to 1.0)."""
discount_amount = price * discount_rate
final_price = price - discount_amount
return final_price
original_price = 100
discount = 0.1 # скидка 10%
final_price = apply_discount(original_price, discount)
print(f"Final price: ${final_price}")
# Output: Final price: $90.0Во второй версии вы сразу видите логику: «Мы вычисляем сумму скидки, затем вычитаем её из цены». В первой версии вам приходится мысленно отслеживать, что означают x и y, и разбираться, что значит x * (1 - y).
40.1.3) Согласованность делает совместную работу возможной
Когда все в команде следуют одним и тем же соглашениям о стиле, код становится предсказуемым. Вы не тратите умственные ресурсы на расшифровку разных форматов — вы можете сосредоточиться на понимании логики.
У Python есть официальное руководство по стилю под названием PEP 8 (Python Enhancement Proposal 8). PEP 8 определяет соглашения для:
- Как именовать переменные, функции и классы
- Как форматировать код (пробелы, длина строки, отступы)
- Когда использовать комментарии и docstring
- Как организовывать импорты
Следование PEP 8 означает, что ваш код будет выглядеть знакомо другим программистам Python, а сотрудничество станет проще. В следующих разделах мы рассмотрим основные рекомендации PEP 8.
40.2) Соглашения об именовании: переменные, функции и классы (PEP 8)
40.2.1) Общие принципы именования
Хорошие имена описательные и однозначные. Они должны говорить, что что-то представляет или делает, без необходимости читать реализацию.
Ключевые принципы:
- Используйте полные слова, а не сокращения (кроме очень распространённых вроде
id,url,html) - Будьте конкретны:
user_countлучше, чемcount,calculate_total_priceлучше, чемcalculate - Избегайте имён из одной буквы, кроме очень коротких циклов или математических формул
- Не включайте информацию о типе в имена (Python имеет динамическую типизацию)
# Плохие имена — непонятно, что они обозначают
# ПРЕДУПРЕЖДЕНИЕ: Плохой стиль — только для демонстрации
# Что такое 'n'? Число? Имя? Узел?
# Что такое 'd'? Дата? Расстояние? Длительность?
# Что такое 'l'? Похоже на цифру 1!
n = "Alice"
d = 25
l = [1, 2, 3]
calc = lambda x: x * 2
# Хорошие имена — ясно и описательно
student_name = "Alice"
age_in_years = 25
test_scores = [1, 2, 3]
double_value = lambda x: x * 2Исключение: короткие переменные цикла
# Допустимо: очень коротко, контекст понятен
for i in range(10):
print(i)
for x, y in coordinates:
distance = (x**2 + y**2) ** 0.5
# Но для ясности лучше предпочитать описательные имена
for student_index in range(len(students)):
print(students[student_index])
for point_x, point_y in coordinates:
distance = (point_x**2 + point_y**2) ** 0.540.2.2) Имена переменных и функций: snake_case
В Python переменные и функции используют snake_case: все буквы строчные, слова разделяются подчёркиваниями.
# Переменные
user_name = "Bob"
total_price = 99.99
is_valid = True
max_retry_count = 3
# Функции
def calculate_tax(amount, rate):
"""Calculate tax on a given amount."""
return amount * rate
def send_email_notification(recipient, message):
"""Send an email to the specified recipient."""
print(f"Sending to {recipient}: {message}")
# Использование функций
tax_amount = calculate_tax(100, 0.08)
send_email_notification("user@example.com", "Welcome!")Почему snake_case? Это очень читаемо. Подчёркивания создают чёткие границы слов, поэтому имена легко «сканировать» взглядом. Сравните calculatetotalprice (трудно читать) и calculate_total_price (понятно сразу).
40.2.3) Имена констант: UPPER_SNAKE_CASE
Константы — значения, которые не должны меняться во время выполнения программы — используют UPPER_SNAKE_CASE: все буквы заглавные, слова разделяются подчёркиваниями.
# Константы на уровне модуля
MAX_LOGIN_ATTEMPTS = 3
DEFAULT_TIMEOUT_SECONDS = 30
PI = 3.14159
DATABASE_URL = "postgresql://localhost/mydb"
def validate_password_length(password):
"""Check if password meets minimum length requirement."""
MIN_PASSWORD_LENGTH = 8 # Константа внутри функции
return len(password) >= MIN_PASSWORD_LENGTH
# Использование констант
if login_attempts > MAX_LOGIN_ATTEMPTS:
print("Account locked")Важно: в Python нет встроенного синтаксиса констант. В отличие от некоторых языков (например, const в JavaScript или final в Java), в Python нет способа объявить, что переменную нельзя изменить.
Вместо этого программисты Python используют соглашение об именовании, чтобы обозначить намерение:
UPPER_SNAKE_CASEозначает: «Я считаю это константой — не изменяйте это»- Это инструмент коммуникации между программистами, а не языковая возможность
# В Python нет синтаксиса констант — это обычная переменная
MAX_LOGIN_ATTEMPTS = 3
# Python не помешает вам изменить её
MAX_LOGIN_ATTEMPTS = 5 # ❌ Технически работает, но нарушает соглашение
# Соглашение об именовании — это сигнал о НАМЕРЕНИИ:
# "Я назвал это ЗАГЛАВНЫМИ буквами, чтобы показать, что не хочу это менять"Лучшая практика: если значение действительно должно меняться во время выполнения программы, не называйте его как константу:
# Это значение будет меняться — используйте строчные буквы
max_login_attempts = 3
max_login_attempts = 5 # ✅ OK — имя показывает, что оно может меняться
# Это значение никогда не должно меняться — используйте ЗАГЛАВНЫЕ
MAX_LOGIN_ATTEMPTS = 3
# Не присваивайте ему новое значение позже в кодеЭто соглашение помогает программистам понимать ваше намерение и избегать ошибок. Когда вы видите MAX_LOGIN_ATTEMPTS, вы знаете, что его не нужно изменять.
40.2.4) Имена классов: PascalCase
Имена классов используют PascalCase (также называется CapWords): каждое слово начинается с заглавной буквы, без подчёркиваний.
# Определения классов
class Student:
"""Represent a student with name and grades."""
def __init__(self, name):
self.name = name
self.grades = []
class ShoppingCart:
"""Manage items in a shopping cart."""
def __init__(self):
self.items = []
def add_item(self, item):
"""Add an item to the cart."""
self.items.append(item)
class DatabaseConnection:
"""Handle database connection and queries."""
def __init__(self, url):
self.url = url
# Создание экземпляров (примечание: для экземпляров используют имена переменных в snake_case)
student = Student("Alice")
shopping_cart = ShoppingCart()
db_connection = DatabaseConnection("localhost")Почему PascalCase для классов? Это визуально отличает классы от функций и переменных. Когда вы видите Student(), вы сразу понимаете, что создаётся экземпляр класса. Когда вы видите calculate_average(), вы понимаете, что вызывается функция.
40.2.5) Приватные и внутренние имена: подчёркивание в начале
Имена, начинающиеся с одного подчёркивания (_name), обозначают внутреннее использование — они предназначены для использования внутри модуля или класса, а не внешним кодом.
В Python нет синтаксиса, чтобы пометить методы или атрибуты как «private» (в отличие от private в Java или C++). Вместо этого Python использует соглашение об именовании с ведущим подчёркиванием (_name), чтобы сообщить о намерении.
Что означает _name:
- «Это только для внутреннего использования»
- «Я сделал это для использования внутри этого класса/модуля, а не внешним кодом»
- «Это может измениться в любой момент в будущих версиях — не полагайтесь на это»
class BankAccount:
"""Represent a bank account with balance tracking."""
def __init__(self, account_number, initial_balance):
self.account_number = account_number
self._balance = initial_balance # Внутренний атрибут
def deposit(self, amount):
"""Add money to the account."""
if self._validate_amount(amount): # Внутренний метод
self._balance += amount
def _validate_amount(self, amount):
"""Internal helper to validate transaction amounts."""
return amount > 0
def get_balance(self):
"""Return the current balance."""
return self._balance
# Использование класса
account = BankAccount("12345", 1000)
account.deposit(500)
print(account.get_balance()) # Output: 1500
# Технически работает, но нарушает соглашение
print(account._balance)
# Output: 1500 (works, but you shouldn't do this!)
# Технически работает, но нарушает соглашение
result = account._validate_amount(100)
# Output: True (works, but you shouldn't do this!)Ключевой момент: Python не может предотвратить доступ к _balance или вызов _validate_amount(). Подчёркивание — это сигнал между программистами, а не функция безопасности.
Зачем существует это соглашение
Поскольку Python не может обеспечивать приватность, подчёркивание — это способ авторов классов сообщить о своём намерении:
Что сигнализирует подчёркивание:
- «Это внутренняя реализация — она может измениться в будущих версиях»
- «Используйте публичные методы — они гарантированно останутся стабильными»
- «Если вы зависите от внутренних деталей, ваш код может сломаться, когда я обновлю библиотеку»
Соглашение создаёт контракт: авторы классов могут свободно менять внутреннюю реализацию (всё, что начинается с _), но должны сохранять стабильным публичный интерфейс. Это позволяет библиотекам развиваться, не ломая пользовательский код.
40.2.6) Специальные имена: двойные подчёркивания
Имена с двойными ведущими и завершающими подчёркиваниями (__name__) — это специальные методы или магические методы (magic methods), определённые Python. Не создавайте собственные имена с таким шаблоном — он зарезервирован для Python.
class Point:
"""Represent a 2D point."""
def __init__(self, x, y): # Специальный метод: инициализация
self.x = x
self.y = y
def __str__(self): # Специальный метод: строковое представление
return f"Point({self.x}, {self.y})"
def __add__(self, other): # Специальный метод: оператор сложения
return Point(self.x + other.x, self.y + other.y)
p1 = Point(1, 2)
p2 = Point(3, 4)
print(p1) # Output: Point(1, 2)
print(p1 + p2) # Output: Point(4, 6)Как мы узнали в главе 31, эти специальные методы позволяют перегрузку операторов и интеграцию со встроенными функциями Python.
40.2.7) Сводная таблица именования
| Тип | Соглашение | Пример |
|---|---|---|
| Переменные | snake_case | user_name, total_count |
| Функции | snake_case | calculate_tax(), send_email() |
| Константы | UPPER_SNAKE_CASE | MAX_SIZE, DEFAULT_TIMEOUT |
| Классы | PascalCase | Student, ShoppingCart |
| Внутренние/приватные | _leading_underscore | _balance, _validate() |
| Специальные/магические | double_underscore | __init__, __str__ |
40.3) Оформление кода: отступы, пробелы и пустые строки
40.3.1) Отступы: четыре пробела
Python использует отступы для определения блоков кода. Всегда используйте 4 пробела на уровень отступа — никогда не используйте табы и никогда не смешивайте табы и пробелы.
def calculate_grade(score):
"""Determine letter grade from numeric score."""
if score >= 90:
return "A"
elif score >= 80:
return "B"
elif score >= 70:
return "C"
else:
return "F"
# Вложенные отступы: 4 пробела на уровень
def process_students(students):
"""Process a list of student records."""
for student in students:
if student["active"]:
grade = calculate_grade(student["score"])
print(f"{student['name']}: {grade}")
students = [
{"name": "Alice", "score": 92, "active": True},
{"name": "Bob", "score": 78, "active": True}
]
process_students(students)
# Output:
# Alice: A
# Bob: CПочему 4 пробела? Это стандарт сообщества Python. Большая часть кода Python, который вы встретите, использует 4 пробела, поэтому следование этому соглашению делает ваш код согласованным с экосистемой.
Настройка редактора: современные редакторы кода можно настроить так, чтобы при нажатии Tab вставлялись 4 пробела. Это даёт удобство клавиши Tab, сохраняя стандарт в 4 пробела.
40.3.2) Максимальная длина строки: 79 символов
PEP 8 рекомендует ограничивать строки 79 символами (и до 99 символов для docstring и комментариев). Это может показаться слишком строгим, но у этого есть практические плюсы:
- Код остаётся читаемым на небольших экранах
- Можно просматривать два файла рядом
- Это поощряет разбиение сложных выражений на более простые части
Примечание: многие современные проекты используют чуть более длинные лимиты (88, 100 или 120 символов). Главное — согласованность внутри вашего проекта. Выберите предел и придерживайтесь его.
# Слишком длинно — трудно читать
# ПРЕДУПРЕЖДЕНИЕ: Плохой стиль — только для демонстрации
def calculate_monthly_payment(principal, annual_rate, years):
return principal * (annual_rate / 12) * (1 + annual_rate / 12) ** (years * 12) / ((1 + annual_rate / 12) ** (years * 12) - 1)
# Лучше — разбито на читаемые строки
def calculate_monthly_payment(principal, annual_rate, years):
"""Calculate monthly loan payment using amortization formula."""
monthly_rate = annual_rate / 12
num_payments = years * 12
numerator = principal * monthly_rate * (1 + monthly_rate) ** num_payments
denominator = (1 + monthly_rate) ** num_payments - 1
return numerator / denominator
payment = calculate_monthly_payment(200000, 0.045, 30)
print(f"Monthly payment: ${payment:.2f}") # Output: Monthly payment: $1013.37Разбиение длинных строк: когда нужно разбить строку, используйте неявное продолжение строки внутри круглых, квадратных или фигурных скобок:
# Длинный вызов функции
result = some_function(
first_argument,
second_argument,
third_argument,
fourth_argument
)
# Длинный список
colors = [
"red", "green", "blue",
"yellow", "orange", "purple",
"pink", "brown", "gray"
]
# Длинная строка
message = (
"This is a very long message that needs to be broken "
"across multiple lines for readability. Python automatically "
"concatenates adjacent string literals."
)
print(message)
# Output: This is a very long message that needs to be broken across multiple lines for readability. Python automatically concatenates adjacent string literals.40.3.3) Пробелы вокруг операторов и после запятых
Используйте пробелы вокруг операторов и после запятых, чтобы улучшить читаемость:
# Плохие пробелы — тесно и трудно читать
# ПРЕДУПРЕЖДЕНИЕ: Плохой стиль — только для демонстрации
x=5
y=x*2+3
result=calculate_tax(100,0.08)
data=[1,2,3,4,5]
# Хорошие пробелы — ясно и читаемо
x = 5
y = x * 2 + 3
result = calculate_tax(100, 0.08)
data = [1, 2, 3, 4, 5]
# Пробелы в выражениях
total = (price * quantity) + shipping_cost
is_valid = (age >= 18) and (has_license == True)
# Пробелы в определениях функций
def calculate_discount(price, discount_rate, minimum_purchase=0):
"""Calculate discounted price if minimum purchase is met."""
if price >= minimum_purchase:
return price * (1 - discount_rate)
return priceИсключение: не ставьте пробелы вокруг = в именованных аргументах или значениях параметров по умолчанию:
# Правильные пробелы для именованных аргументов
result = calculate_discount(price=100, discount_rate=0.1, minimum_purchase=50)
# Правильные пробелы для параметров по умолчанию
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"40.3.4) Пустые строки для логического разделения
Используйте пустые строки, чтобы разделять логические части кода:
Две пустые строки между функциями и классами верхнего уровня:
def first_function():
"""First function."""
pass
def second_function():
"""Second function."""
pass
class MyClass:
"""A class definition."""
passОдна пустая строка между методами внутри класса:
class Student:
"""Represent a student with grades."""
def __init__(self, name):
self.name = name
self.grades = []
def add_grade(self, grade):
"""Add a grade to the student's record."""
self.grades.append(grade)
def get_average(self):
"""Calculate the student's grade average."""
if not self.grades:
return 0
return sum(self.grades) / len(self.grades)Пустые строки внутри функций для разделения логических шагов:
def process_order(order_items, customer):
"""Process a customer order and calculate total."""
# Рассчитать промежуточную сумму
subtotal = 0
for item in order_items:
subtotal += item["price"] * item["quantity"]
# Применить скидку клиента
discount = 0
if customer["is_premium"]:
discount = subtotal * 0.1
# Рассчитать налог
tax = (subtotal - discount) * 0.08
# Рассчитать итоговую сумму
total = subtotal - discount + tax
return {
"subtotal": subtotal,
"discount": discount,
"tax": tax,
"total": total
}Эти пустые строки работают как визуальные «абзацы», делая структуру кода очевидной сразу.
40.3.5) Избегание пробелов в конце строк
Не оставляйте пробелы в конце строк — они невидимы, но могут вызывать проблемы в системах контроля версий и некоторых редакторах.
# Плохо — невидимые пробелы в конце строк (показаны как · для иллюстрации)
# ПРЕДУПРЕЖДЕНИЕ: Плохой стиль — только для демонстрации
def calculate(x):···
return x * 2···
# Хорошо — нет пробелов в конце строк
def calculate(x):
return x * 2Большинство современных редакторов можно настроить так, чтобы они автоматически удаляли пробелы в конце строк при сохранении файла.
40.4) Документация: написание полезных комментариев и docstring
40.4.1) Когда писать комментарии
Комментарии объясняют, почему код что-то делает, а не что он делает. Хорошо названные переменные и функции должны делать «что» очевидным.
# Плохой комментарий — говорит очевидное
# ПРЕДУПРЕЖДЕНИЕ: Плохой стиль — только для демонстрации
x = x + 1 # Добавить 1 к x
# Хороший комментарий — объясняет почему
x = x + 1 # Поправка для индексации с нуля
# Плохой комментарий — дублирует код
# ПРЕДУПРЕЖДЕНИЕ: Плохой стиль — только для демонстрации
# Проверить, что age больше или равно 18
if age >= 18:
print("Adult")
# Хороший комментарий — объясняет бизнес-логику
# Законный возраст употребления алкоголя в США
if age >= 21:
print("Can purchase alcohol")Когда комментарии полезны:
- Объяснение сложных алгоритмов:
def binary_search(sorted_list, target):
"""Search for target in sorted list using binary search."""
left = 0
right = len(sorted_list) - 1
while left <= right:
# Вычислить середину, избегая переполнения целых чисел
# (right + left) // 2 может переполниться при очень больших индексах
mid = left + (right - left) // 2
if sorted_list[mid] == target:
return mid
elif sorted_list[mid] < target:
left = mid + 1 # Цель находится в правой половине
else:
right = mid - 1 # Цель находится в левой половине
return -1 # Цель не найдена- Прояснение неочевидных бизнес-правил:
def calculate_shipping_cost(weight, distance):
"""Calculate shipping cost based on weight and distance."""
base_cost = 5.00
# Акция бесплатной доставки для тяжёлых товаров (политика компании на 2024 год)
# Это стимулирует оптовые заказы и снижает стоимость доставки на единицу товара
if weight > 50:
return 0
# Стандартный тариф: $0.50 за фунт плюс $0.10 за милю
# На основе договора с перевозчиком, согласованного в Q1 2024
return base_cost + (weight * 0.50) + (distance * 0.10)- Документирование обходных решений или временных решений:
def process_data(data):
"""Process incoming data records."""
# TODO: Это временное исправление для некорректных записей
# Удалить после того, как проверка данных будет реализована выше по цепочке
if not isinstance(data, list):
data = [data]
for record in data:
# Обработать каждую запись
pass40.4.2) Написание эффективных docstring
Docstring — это специальные комментарии, документирующие модули, классы и функции. Они заключены в тройные кавычки и идут первой инструкцией в определении.
def calculate_bmi(weight_kg, height_m):
"""
Calculate Body Mass Index (BMI).
BMI is calculated as weight in kilograms divided by the square of height in meters.
Args:
weight_kg: Weight in kilograms (float or int)
height_m: Height in meters (float or int)
Returns:
float: The calculated BMI value
Example:
>>> calculate_bmi(70, 1.75)
22.857142857142858
"""
return weight_kg / (height_m ** 2)
# Доступ к docstring
print(calculate_bmi.__doc__)
# Output:
# Calculate Body Mass Index (BMI).
#
# BMI is calculated as weight in kilograms divided by the square of height in meters.
# ...Однострочные docstring для простых функций:
def square(x):
"""Return the square of x."""
return x * x
def is_even(n):
"""Return True if n is even, False otherwise."""
return n % 2 == 0Многострочные docstring для сложных функций:
def find_prime_factors(n):
"""
Find all prime factors of a positive integer.
This function returns a list of prime numbers that, when multiplied
together, equal the input number. The factors are returned in ascending order.
Args:
n: A positive integer greater than 1
Returns:
list: Prime factors in ascending order
Raises:
ValueError: If n is less than 2
Example:
>>> find_prime_factors(12)
[2, 2, 3]
>>> find_prime_factors(17)
[17]
"""
if n < 2:
raise ValueError("n must be at least 2")
factors = []
divisor = 2
while n > 1:
while n % divisor == 0:
factors.append(divisor)
n = n // divisor
divisor += 1
return factorsDocstring классов:
class BankAccount:
"""
Represent a bank account with deposit and withdrawal operations.
This class maintains an account balance and provides methods for
depositing and withdrawing money. All transactions are validated to prevent negative balances.
Attributes:
account_number: Unique identifier for the account
balance: Current account balance in dollars
"""
def __init__(self, account_number, initial_balance=0):
"""
Initialize a new bank account.
Args:
account_number: Unique account identifier (string)
initial_balance: Starting balance (default: 0)
"""
self.account_number = account_number
self.balance = initial_balance
def deposit(self, amount):
"""
Add money to the account.
Args:
amount: Amount to deposit (must be positive)
Raises:
ValueError: If amount is not positive
"""
if amount <= 0:
raise ValueError("Deposit amount must be positive")
self.balance += amount40.4.3) Соглашения для docstring
Первая строка: краткое резюме того, что делает функция/класс. Должна помещаться в одну строку.
Пустая строка: отделяет резюме от подробного описания.
Подробное описание: объясняет, что делает функция, важные детали и как ей пользоваться.
Args/Parameters: перечисляет каждый параметр с его типом и назначением.
Returns: описывает, что возвращает функция, и тип.
Raises: документирует исключения, которые может выбросить функция.
Example: показывает типичное использование (необязательно, но полезно).
def calculate_compound_interest(principal, rate, time, compounds_per_year=1):
"""
Calculate compound interest on an investment.
Uses the compound interest formula: A = P(1 + r/n)^(nt)
where A is the final amount, P is principal, r is annual rate,
n is compounds per year, and t is time in years.
Args:
principal: Initial investment amount (float)
rate: Annual interest rate as decimal (e.g., 0.05 for 5%)
time: Investment period in years (float)
compounds_per_year: Number of times interest compounds annually
(default: 1 for annual compounding)
Returns:
float: Final amount after compound interest
Example:
>>> calculate_compound_interest(1000, 0.05, 10, 12)
1647.0095
"""
return principal * (1 + rate / compounds_per_year) ** (compounds_per_year * time)40.4.4) Комментарии TODO для будущих задач
Используйте комментарии TODO, чтобы отмечать места, которые потребуют внимания в будущем:
def process_payment(amount, payment_method):
"""Process a payment transaction."""
# TODO: Добавить поддержку платежей в криптовалюте
# TODO: Реализовать проверки на мошенничество
if payment_method == "credit_card":
return process_credit_card(amount)
elif payment_method == "paypal":
return process_paypal(amount)
else:
raise ValueError(f"Unsupported payment method: {payment_method}")Многие редакторы умеют искать комментарии TODO, поэтому легко находить места, которые требуют доработки.
40.5) Организация кода: импорты, константы, функции и main
40.5.1) Стандартная структура модуля
Хорошо организованный модуль Python следует такой структуре:
- Docstring модуля: описывает, что делает модуль
- Импорты: стандартная библиотека, сторонние пакеты, затем локальные импорты
- Константы: константы на уровне модуля
- Функции и классы: основной код
- Блок выполнения main: код, который запускается при выполнении скрипта
"""
student_manager.py
Manage student records including grades and GPA calculations.
This module provides functions for adding students, recording grades,
and calculating grade point averages.
"""
# Импорты стандартной библиотеки
import sys
from datetime import datetime
# Импорты сторонних библиотек (если есть)
# import requests
# Локальные импорты (если есть)
# from .database import save_student
# Константы
MAX_GRADE = 100
MIN_GRADE = 0
PASSING_GRADE = 60
# Функции
def calculate_gpa(grades):
"""
Calculate GPA from a list of numeric grades.
Args:
grades: List of numeric grades (0-100)
Returns:
float: GPA on 4.0 scale
"""
if not grades:
return 0.0
average = sum(grades) / len(grades)
# Преобразовать к шкале 4.0
if average >= 90:
return 4.0
elif average >= 80:
return 3.0
elif average >= 70:
return 2.0
elif average >= 60:
return 1.0
else:
return 0.0
def validate_grade(grade):
"""
Check if a grade is within valid range.
Args:
grade: Numeric grade to validate
Returns:
bool: True if grade is valid, False otherwise
"""
return MIN_GRADE <= grade <= MAX_GRADE
# Выполнение main
if __name__ == "__main__":
# Код, который выполняется при прямом запуске скрипта
test_grades = [85, 92, 78, 88]
gpa = calculate_gpa(test_grades)
print(f"GPA: {gpa}") # Output: GPA: 3.040.5.2) Организация импортов
Группируйте импорты в три секции, разделённые пустыми строками:
- Импорты стандартной библиотеки: встроенные модули Python
- Импорты сторонних библиотек: установленные пакеты (например,
requests,numpy) - Локальные импорты: ваши собственные модули
# Импорты стандартной библиотеки
import os
import sys
from datetime import datetime, timedelta
from pathlib import Path
# Импорты сторонних библиотек
import requests
from flask import Flask, render_template
# Импорты локального приложения
from myapp.database import connect_db
from myapp.models import User, Product
from myapp.utils import format_currencyСтили импорта:
# Импортировать весь модуль
import math
result = math.sqrt(16) # Output: 4.0
# Импортировать конкретные элементы
from math import sqrt, pi
result = sqrt(16) # Output: 4.0
# Импортировать с псевдонимом
import numpy as np
array = np.array([1, 2, 3])
# Импортировать несколько элементов
from os import path, getcwd, listdirИзбегайте импортов со звёздочкой (from module import *) — они делают непонятным, откуда берутся имена:
# Плохо — непонятно, откуда берётся sqrt
# ПРЕДУПРЕЖДЕНИЕ: Плохой стиль — только для демонстрации
from math import *
result = sqrt(16)
# Хорошо — явный импорт
from math import sqrt
result = sqrt(16)40.5.3) Организация констант
Размещайте константы на уровне модуля ближе к началу, после импортов:
"""Configuration settings for the application."""
import os
# Константы приложения
APP_NAME = "Student Manager"
VERSION = "1.0.0"
DEBUG_MODE = True
# Конфигурация базы данных
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///students.db")
MAX_CONNECTIONS = 10
# Бизнес-правила
MAX_STUDENTS_PER_CLASS = 30
PASSING_GRADE = 60
GRADE_WEIGHTS = {
"homework": 0.3,
"midterm": 0.3,
"final": 0.4
}
def calculate_final_grade(homework, midterm, final):
"""Calculate weighted final grade."""
return (
homework * GRADE_WEIGHTS["homework"] +
midterm * GRADE_WEIGHTS["midterm"] +
final * GRADE_WEIGHTS["final"]
)40.5.4) Логический порядок функций
Организуйте функции в логическом порядке:
- Сначала публичные функции: функции, предназначенные для использования другими модулями
- Затем вспомогательные функции: внутренние функции, которые поддерживают публичные
- Связанные функции рядом: группируйте функции, которые работают вместе
"""Order processing module."""
# Функции публичного API
def process_order(order_items, customer):
"""
Process a customer order.
This is the main entry point for order processing.
"""
subtotal = _calculate_subtotal(order_items)
discount = _calculate_discount(subtotal, customer)
tax = _calculate_tax(subtotal - discount)
total = subtotal - discount + tax
return {
"subtotal": subtotal,
"discount": discount,
"tax": tax,
"total": total
}
def validate_order(order_items):
"""Validate that an order contains valid items."""
if not order_items:
return False
for item in order_items:
if not _validate_item(item):
return False
return True
# Внутренние вспомогательные функции
def _calculate_subtotal(items):
"""Calculate order subtotal (internal use)."""
total = 0
for item in items:
total += item["price"] * item["quantity"]
return total
def _calculate_discount(subtotal, customer):
"""Calculate customer discount (internal use)."""
if customer.get("is_premium"):
return subtotal * 0.1
return 0
def _calculate_tax(amount):
"""Calculate sales tax (internal use)."""
TAX_RATE = 0.08
return amount * TAX_RATE
def _validate_item(item):
"""Validate a single order item (internal use)."""
required_fields = ["name", "price", "quantity"]
return all(field in item for field in required_fields)Обратите внимание, что публичные функции (process_order, validate_order) идут первыми, а вспомогательные функции (с префиксом _) — после. Это делает понятным, какие функции являются основным API.
40.5.5) Организация классов внутри модулей
Когда модуль содержит классы, организуйте их логично:
"""User management system."""
# Константы
DEFAULT_ROLE = "user"
ADMIN_ROLE = "admin"
# Сначала базовые классы
class User:
"""Base user class."""
def __init__(self, username, email):
self.username = username
self.email = email
self.role = DEFAULT_ROLE
def can_edit(self, resource):
"""Check if user can edit a resource."""
return resource.owner == self.username
# Затем производные классы после базовых
class AdminUser(User):
"""Administrator with elevated privileges."""
def __init__(self, username, email):
super().__init__(username, email)
self.role = ADMIN_ROLE
def can_edit(self, resource):
"""Admins can edit any resource."""
return True
# Связанные классы сгруппированы вместе
class Resource:
"""Represent a resource that can be owned and edited."""
def __init__(self, name, owner):
self.name = name
self.owner = owner
# Вспомогательные функции, связанные с классами
def create_user(username, email, is_admin=False):
"""Factory function to create appropriate user type."""
if is_admin:
return AdminUser(username, email)
return User(username, email)Принципы организации классов:
- Базовые классы перед производными (читателю нужно сначала понять базовый)
- Связанные классы группируются вместе (User и Resource связаны)
- Вспомогательные функции, работающие с классами, идут после определений классов
- У каждого класса должен быть понятный docstring, объясняющий его назначение
40.6) Шаблон if name == "main"
40.6.1) Понимание шаблона
У каждого файла Python есть встроенная переменная __name__. Python автоматически устанавливает значение этой переменной в зависимости от того, как используется файл:
- Когда вы запускаете файл напрямую (например,
python my_script.py), Python устанавливает__name__в"__main__" - Когда вы импортируете файл как модуль, Python устанавливает
__name__в имя модуля (имя файла без.py)
Это позволяет писать код, который выполняется только когда файл запускают напрямую, а не когда его импортируют:
"""math_utils.py - Mathematical utility functions."""
def add(a, b):
"""Add two numbers."""
return a + b
def multiply(a, b):
"""Multiply two numbers."""
return a * b
# Этот код выполняется только при прямом запуске файла
if __name__ == "__main__":
# Протестировать функции
print(f"5 + 3 = {add(5, 3)}") # Output: 5 + 3 = 8
print(f"5 * 3 = {multiply(5, 3)}") # Output: 5 * 3 = 15Когда вы запускаете python math_utils.py, вы увидите вывод. Но когда вы импортируете его в другом файле:
# another_file.py
from math_utils import add, multiply
result = add(10, 20)
print(result) # Output: 30
# Тестовый код из math_utils.py НЕ выполняетсяОбратите внимание, что тестовый код (внутри if __name__ == "__main__":) НЕ выполняется при импорте!
40.6.2) Почему этот шаблон важен
Этот шаблон служит нескольким важным целям:
1. Тестирование и демонстрация: вы можете включать примеры использования в тот же файл, что и ваши функции:
"""temperature.py - Temperature conversion utilities."""
def celsius_to_fahrenheit(celsius):
"""Convert Celsius to Fahrenheit."""
return (celsius * 9/5) + 32
def fahrenheit_to_celsius(fahrenheit):
"""Convert Fahrenheit to Celsius."""
return (fahrenheit - 32) * 5/9
if __name__ == "__main__":
# Продемонстрировать функции
print("Temperature Conversion Examples:")
print(f"0°C = {celsius_to_fahrenheit(0)}°F") # Output: 0°C = 32.0°F
print(f"100°C = {celsius_to_fahrenheit(100)}°F") # Output: 100°C = 212.0°F
print(f"32°F = {fahrenheit_to_celsius(32)}°C") # Output: 32°F = 0.0°C2. Переиспользуемые модули: один и тот же файл может быть и самостоятельным скриптом, и импортируемым модулем:
"""data_processor.py - Process and analyze data files."""
import sys
def load_data(filename):
"""Load data from a file."""
with open(filename) as f:
return [line.strip() for line in f]
def analyze_data(data):
"""Perform analysis on data."""
return {
"count": len(data),
"average_length": sum(len(item) for item in data) / len(data)
}
if __name__ == "__main__":
# При запуске как скрипта обработать аргументы командной строки
if len(sys.argv) < 2:
print("Usage: python data_processor.py <filename>")
sys.exit(1)
filename = sys.argv[1]
data = load_data(filename)
results = analyze_data(data)
print(f"Processed {results['count']} items")
print(f"Average length: {results['average_length']:.2f}")Вы можете запустить это как скрипт:
$ python data_processor.py data.txt
Processed 42 items
Average length: 15.23Или импортировать в другом файле:
# my_analysis.py
from data_processor import load_data, analyze_data
my_data = load_data("myfile.txt")
results = analyze_data(my_data)
print(f"Found {results['count']} items")40.6.3) Распространённые шаблоны для main-блоков
Шаблон 1: простые тестовые случаи
"""calculator.py - Basic calculator operations."""
def add(a, b):
"""Add two numbers."""
return a + b
def subtract(a, b):
"""Subtract b from a."""
return a - b
if __name__ == "__main__":
# Быстрые тесты
assert add(2, 3) == 5
assert subtract(10, 4) == 6
print("All tests passed!") # Output: All tests passed!Шаблон 2: функция main
Для более сложных скриптов определите функцию main():
"""report_generator.py - Generate reports from data."""
import sys
def load_data(filename):
"""Load data from file."""
# Implementation here
pass
def generate_report(data):
"""Generate report from data."""
# Implementation here
pass
def save_report(report, output_file):
"""Save report to file."""
# Implementation here
pass
def main():
"""Main entry point for the script."""
if len(sys.argv) < 3:
print("Usage: python report_generator.py <input> <output>")
return 1
input_file = sys.argv[1]
output_file = sys.argv[2]
try:
data = load_data(input_file)
report = generate_report(data)
save_report(report, output_file)
print(f"Report saved to {output_file}")
return 0
except Exception as e:
print(f"Error: {e}")
return 1
if __name__ == "__main__":
# Завершить с кодом статуса из main (0 = успех, 1 = ошибка)
sys.exit(main())У этого шаблона есть несколько преимуществ:
- Функцию
main()можно тестировать независимо - Понятная точка входа для скрипта
- Корректные коды завершения (0 для успеха, ненулевые для ошибок)
- Чёткое разделение между логикой скрипта и функциями модуля
40.6.4) Лучшие практики для main-блоков
Держите main-блоки сфокусированными: код внутри if __name__ == "__main__" должен в основном управлять выполнением скрипта, а не содержать сложную логику:
# Плохо — сложная логика в main-блоке
# ПРЕДУПРЕЖДЕНИЕ: Плохой стиль — только для демонстрации
if __name__ == "__main__":
data = []
for i in range(100):
if i % 2 == 0:
data.append(i * 2)
result = sum(data) / len(data)
print(result)
# Хорошо — логика в функциях, main-блок координирует
def generate_even_doubles(limit):
"""Generate doubled even numbers up to limit."""
return [i * 2 for i in range(limit) if i % 2 == 0]
def calculate_average(numbers):
"""Calculate average of numbers."""
return sum(numbers) / len(numbers)
if __name__ == "__main__":
data = generate_even_doubles(100)
result = calculate_average(data)
print(result) # Output: 99.0Используйте функцию main() для сложных скриптов: как показано выше, определение функции main() делает ваш скрипт более тестируемым и организованным.
Документируйте использование скрипта: если ваш скрипт принимает аргументы командной строки, документируйте их в docstring модуля:
"""
file_processor.py - Process text files with various operations.
Usage:
python file_processor.py <input_file> <output_file> [--uppercase]
Arguments:
input_file: Path to input file
output_file: Path to output file
--uppercase: Convert text to uppercase (optional)
"""
import sys
def process_file(input_path, output_path, uppercase=False):
"""Process file with specified options."""
with open(input_path) as f:
content = f.read()
if uppercase:
content = content.upper()
with open(output_path, 'w') as f:
f.write(content)
if __name__ == "__main__":
if len(sys.argv) < 3:
print(__doc__) # Вывести docstring модуля
sys.exit(1)
input_file = sys.argv[1]
output_file = sys.argv[2]
uppercase = "--uppercase" in sys.argv
process_file(input_file, output_file, uppercase)
print(f"Processed {input_file} -> {output_file}")Написание чистого, читаемого кода — это навык, который развивается с практикой. Соглашения и шаблоны в этой главе — не произвольные правила, а проверенные практики, которые делают код проще для понимания, сопровождения и отладки. По мере того как вы будете писать больше кода на Python, эти паттерны станут для вас естественными.
Помните: код читают намного чаще, чем пишут. Несколько лишних секунд, потраченных на выбор понятного имени, добавление полезного комментария или правильную организацию импортов, сэкономят часы путаницы позже — для вас и для других, кто будет работать с вашим кодом.
В следующей главе мы рассмотрим техники отладки и тестирования, которые опираются на эти практики чистого кода и помогают писать не просто читаемый код, но и корректный и надёжный код.