Чистый код A: хорошие имена, маленькие функции и единственная ответственность
- Объяснить, что такое Clean Code и почему код читают гораздо чаще, чем пишут
- Выбирать понятные имена для переменных, функций и файлов по соглашениям Python
- Распознавать функцию, которая делает больше одного дела, и разбивать её
- Применять принцип Single Responsibility на уровне функций
Что мы изучим?
До сих пор целью было «чтобы код работал». С сегодняшнего дня цель — чтобы он был ещё и понятным. Мы изучим:
- Именование (naming) — как выбирать имена для переменных, функций и файлов.
- Маленькие функции (small functions) — почему функции должны быть короткими.
- Единственную ответственность (single responsibility) — каждая функция делает одно дело.
Зачем это нужно?
Код читают гораздо чаще, чем пишут. Функцию пишут один раз, а читают при каждом исправлении бага, каждой новой функциональности и каждом Code Review коллеги. А через месяц и вы сами будете читать её как посторонний.
В компаниях код, который работает, но нечитаем, отклоняют на Code Review. На собеседованиях проверяют не только «решили ли вы», но и «можно ли понять решение».
Сравните:
def f(l):
t = 0
for x in l:
t = t + x[1] * x[2]
return t * 1.17VAT_RATE = 1.17
def calculate_order_total(items):
subtotal = 0
for item in items:
subtotal = subtotal + item["price"] * item["quantity"]
return subtotal * VAT_RATEОбе функции делают абсолютно одно и то же. Вторую понимаешь без усилий.
Объяснение с основ
Что такое Clean Code?
Clean Code (чистый код) — код, который другой человек может легко и уверенно прочитать, понять и изменить. Это не набор жёстких правил, а набор привычек, которые делают код понятным.
Именование — хорошие имена
Хорошее имя отвечает на вопрос «что это?» или «что это делает?» без необходимости читать окружающий код.
Соглашения об именах в Python (по PEP 8, официальному руководству по стилю):
| Что | Стиль | Пример |
|---|---|---|
| Переменные и функции | snake_case | total_price, send_email() |
| Константы (значения, которые не меняются) | UPPER_SNAKE_CASE | VAT_RATE, MAX_RETRIES |
| Классы (тема ООП) | PascalCase | Student, BankAccount |
| Файлы и модули | snake_case.py | order_utils.py |
Правила хорошего имени:
- Описывает намерение —
days_until_examлучше, чемd. - Функция = глагол —
calculate_total(),send_invoice(),is_valid_email(). Переменная = существительное —total,invoice,email. - Логическое значение звучит как вопрос —
is_active,has_permission,can_edit. - Список — во множественном числе —
students(список),student(один элемент). - Без непонятных сокращений —
customer, а неcstmr. (Известные сокращения вродеid,url,maxдопустимы.) - Без «магических чисел» (magic numbers) — вместо
* 1.17, смысла которого никто не знает, — именованная константа:VAT_RATE = 1.17.
Маленькие функции
Длинную функцию (50+ строк) трудно понять, трудно протестировать и трудно изменить. Эмпирическое правило: если функция не помещается на один экран или ей трудно дать имя без слова «и», она, вероятно, делает слишком много.
Единственная ответственность
Принцип единственной ответственности (Single Responsibility Principle, SRP): у каждой функции должна быть одна причина для изменения, то есть одна задача.
В ресторане есть официант, повар и кассир. Если повар ещё и принимает заказы, и готовит, и берёт деньги, то любое изменение (новое меню, новый кардридер) требует «менять» его, а каждая его ошибка вредит всему. Когда у каждого одна задача, легко заменить, улучшить и найти, где проблема.
Синтаксис
Для чистого кода нет «нового синтаксиса». Это тот же Python, просто организованный по-другому. Шаблон:
CONSTANT_NAME = value # constants at the top, UPPER_SNAKE_CASE
def verb_noun(clear_parameter): # a function does ONE thing, named by what it does
...
return result
def main(): # one function that connects the small pieces
data = get_something()
result = process_something(data)
show_something(result)
main()Первый пример кода
До — код, который работает, но не чистый. week-2/grades_before.py:
def do(g):
s = 0
for x in g:
s = s + x
a = s / len(g)
if a >= 55:
print("avg", a, "pass")
else:
print("avg", a, "fail")
do([80, 45, 92, 60])После — то же поведение, чистый код. week-2/grades_after.py:
PASSING_GRADE = 55
def calculate_average(grades):
total = 0
for grade in grades:
total = total + grade
return total / len(grades)
def is_passing(average):
return average >= PASSING_GRADE
def print_report(average):
if is_passing(average):
status = "pass"
else:
status = "fail"
print("Average:", average, "-", status)
student_grades = [80, 45, 92, 60]
average = calculate_average(student_grades)
print_report(average)Вывод (версия «после». Старая версия печатает avg 69.25 pass — тот же расчёт, сформулированный менее понятно):
Average: 69.25 - passРазбор кода построчно
| В старом коде | В чистом коде | Почему это лучше |
|---|---|---|
do | Три функции с именами | Имя говорит, что делает каждая. |
g, s, x, a | grades, total, grade, average | Не нужно «расшифровывать». |
55 внутри кода | PASSING_GRADE = 55 | Значение со смыслом и именем. Если проходной балл изменится, меняете в одном месте. |
| Расчёт + решение + печать в одной функции | calculate_average, is_passing, print_report | Каждая функция делает одно дело. |
Практическое преимущество: хотите показывать среднее на сайте, а не на экране? Меняется только print_report. Хотите изменить проходной балл? Только PASSING_GRADE. А calculate_average пригодится и в других местах, потому что она возвращает значение, а не печатает его.
Ещё один пример
Функция «которая делает всё» в системе регистрации, и как её разбить. До:
def register(n, e, a):
if "@" not in e:
print("bad email")
return
if a < 16:
print("too young")
return
u = n.strip().lower().replace(" ", ".")
print("Welcome " + n + "! Your username is " + u)После:
MIN_AGE = 16
def is_valid_email(email):
return "@" in email
def is_old_enough(age):
return age >= MIN_AGE
def create_username(full_name):
return full_name.strip().lower().replace(" ", ".")
def register_user(full_name, email, age):
if not is_valid_email(email):
print("Invalid email address")
return
if not is_old_enough(age):
print("You must be at least", MIN_AGE, "to register")
return
username = create_username(full_name)
print("Welcome " + full_name + "! Your username is " + username)
register_user("Dana Cohen", "dana@mail.com", 24)
register_user("Avi Levi", "avi-mail.com", 30)Вывод:
Welcome Dana Cohen! Your username is dana.cohen
Invalid email addressРазбор:
"@" in email—inпроверяет, встречается ли кусок текста внутри другого текста, и возвращаетTrue/False..strip()— убирает пробелы в начале и в конце..lower()— строчные буквы..replace(" ", ".")— заменяет пробелы точками. Это методыstr, которые можно вызывать цепочкой один за другим.returnбез значения — немедленно выходит из функции (возвращаетNone). Так мы не продолжаем регистрацию после ошибки. Этот приём называется Early Return (или Guard Clause).register_userтеперь «читается как рассказ»: проверить email, проверить возраст, создать имя пользователя, поприветствовать. Все детали — в маленьких функциях.
Типичная ошибка
Ошибка 1: имена, которые лгут.
def get_user(user_id):
user = find_in_list(user_id)
user["last_login"] = "today" # changes data!
delete_old_sessions(user) # deletes data!
return userИмя говорит «получить», а функция изменяет и удаляет. Тот, кто вызовет её, чтобы «просто получить пользователя», будет удивлён. Имя должно описывать всё, что делает функция. А если это трудно описать без «и», то ответственность не одна.
Ошибка 2: чрезмерное дробление. Обратное — тоже проблема:
def add_one(number):
return number + 1Однострочная функция, которая скрывает очевидную операцию, только добавляет переходов при чтении. Разбивайте, когда есть идея с именем (is_old_enough), а не каждую строку.
Ошибка 3: слишком общие имена. data, info, temp, result, manager, process(). Такие имена почти всегда можно улучшить: data → student_records, process() → apply_discount().
Практика
Упражнения опираются только на то, что пройдено в этом уроке и в предыдущих. Сначала попробуйте сами, и только потом открывайте решение.
Упражнение 1: Имена, которые объясняют сами себя
Следующий код работает, но понять, что он делает, невозможно:
def f(a, b):
c = a * b
if c > 500:
c = c - 50
return c
print(f(120, 5))
print(f(30, 2))Перепишите его с хорошими именами для функции и переменных и замените числа 500 и 50 именованными константами. Вывод должен остаться прежним:
550
60Показать решение
DISCOUNT_THRESHOLD = 500
DISCOUNT_AMOUNT = 50
def calculate_order_price(unit_price, quantity):
price = unit_price * quantity
if price > DISCOUNT_THRESHOLD:
price = price - DISCOUNT_AMOUNT
return price
print(calculate_order_price(120, 5))
print(calculate_order_price(30, 2))550
60- Имя функции — глагол, который описывает, что она делает.
500и50были магическими числами — теперь ясно, что это, и если скидка изменится, меняете в одном месте.
Упражнение 2: Соглашения об именах
Для каждого имени — записано ли оно по соглашениям Python? Если нет, напишите правильное имя.
TotalPrice (a variable)
numberofstudents (a variable)
max_grade (a variable)
vat_rate (a constant that never changes)
GetAverage (a function)Показать решение
| Имя | Правильно? | Правильное имя |
|---|---|---|
TotalPrice | Нет — переменные пишут в snake_case | total_price |
numberofstudents | Нет — трудно читать без подчёркиваний | number_of_students |
max_grade | Да | — |
vat_rate | Нет — константы пишут заглавными буквами | VAT_RATE |
GetAverage | Нет — функции пишут в snake_case, и лучше глагол | get_average / calculate_average |
Упражнение 3: Одна функция — одна ответственность
Следующая функция вычисляет среднее, решает, сдал ли студент, и ещё печатает — три задачи:
def report(name, grades):
total = 0
for g in grades:
total = total + g
avg = total / len(grades)
if avg >= 55:
print(name + ": average", avg, "- passed")
else:
print(name + ": average", avg, "- failed")
report("Dana", [80, 85])
report("Avi", [50, 46])Разбейте её на три функции: calculate_average, is_passing и print_report(name, average). print_report получает уже вычисленное среднее и только печатает. Вывод должен остаться:
Dana: average 82.5 - passed
Avi: average 48.0 - failedПоказать решение
PASSING_GRADE = 55
def calculate_average(grades):
return sum(grades) / len(grades)
def is_passing(average):
return average >= PASSING_GRADE
def print_report(name, average):
if is_passing(average):
status = "passed"
else:
status = "failed"
print(name + ": average", average, "-", status)
dana_average = calculate_average([80, 85])
print_report("Dana", dana_average)
avi_average = calculate_average([50, 46])
print_report("Avi", avi_average)Dana: average 82.5 - passed
Avi: average 48.0 - failedcalculate_averageтолько вычисляет,is_passingтолько решает, аprint_reportполучает готовое среднее и только показывает его. Вычисление происходит вне её.- Если бы
print_reportсама вызывалаcalculate_average, она снова и вычисляла бы, и печатала. Код короче, но ответственности не разделены. - Теперь
calculate_averageможно использовать и там, где печатать не нужно.
Упражнение 4: Без магических чисел
В интернет-магазине следующая функция получает сумму заказа и возвращает сумму к оплате:
def final_price(total):
if total < 200:
total = total + 30
return total
print(final_price(150))
print(final_price(250))- Что означают 200 и 30 в магазине? (Подсказка: что происходит с маленьким заказом?) Дайте им имена в виде констант.
- Перепишите функцию так, чтобы её имя точно объясняло, что она добавляет.
Ожидаемый вывод:
180
250Показать решение
FREE_SHIPPING_MINIMUM = 200
SHIPPING_FEE = 30
def add_shipping(order_total):
if order_total < FREE_SHIPPING_MINIMUM:
return order_total + SHIPPING_FEE
return order_total
print(add_shipping(150))
print(add_shipping(250))180
250200 — минимальная сумма для бесплатной доставки, а 30 — стоимость доставки. С именами никому не нужно гадать.
Упражнение 5: Маленькая функция, которая отвечает на один вопрос
Напишите функцию is_valid_password(password), которая возвращает True, если длина пароля не меньше 8 символов и в нём есть хотя бы одна цифра, иначе False. Функция не печатает — только возвращает.
print(is_valid_password("abc12345"))
print(is_valid_password("abcdefgh"))
print(is_valid_password("a1"))Ожидаемый вывод:
True
False
FalseПоказать решение
MIN_PASSWORD_LENGTH = 8
def has_digit(text):
for char in text:
if char.isdigit():
return True
return False
def is_valid_password(password):
return len(password) >= MIN_PASSWORD_LENGTH and has_digit(password)
print(is_valid_password("abc12345"))
print(is_valid_password("abcdefgh"))
print(is_valid_password("a1"))True
False
FalseИмя is_valid_password начинается с is_ — ясно, что она возвращает True или False. Проверка цифры вынесена в собственную маленькую функцию.
Итоги
- Clean Code = код, который другие (и вы через месяц) легко понимают и меняют. Код читают гораздо чаще, чем пишут.
- Имена:
snake_caseдля переменных и функций,UPPER_SNAKE_CASEдля констант. Функция = глагол, логическое значение = вопрос (is_...). - Без магических чисел. Значение со смыслом получает именованную константу.
- Маленькие функции с единственной ответственностью (SRP). Если в имени нужно «и», вероятно, нужно разбить.
- Функция, которая вычисляет, — возвращает. Печать — отдельная ответственность.