Введение
Ты пишешь по-английски каждый рабочий день, даже если ни разу не открывал переписку с иностранным коллегой. Это имена в коде. Переменные, функции, классы, поля таблиц, ключи JSON, эндпоинты — английские слова, поставленные в определённом порядке. И читают их чаще любого другого текста в проекте: документацию открывают раз в месяц, а имя функции видит каждый, кто открыл файл.
Отсюда неприятное следствие. Ошибка в письме коллеге живёт один день и вызывает разве что лёгкое недоумение. Ошибка в имени функции живёт годами, тиражируется копипастом, уезжает в публичный API — и каждый новый человек в команде тратит своё первое утро на выяснение, чем process_data отличается от handle_data. Ответ, как правило, «ничем». Плохое имя — это долг, который платят все и всегда.
Разберём семантику близких глаголов (почему fetch — не то же самое, что get), правила булевых имён, порядок слов в идентификаторе, слова-паразиты, конвенции комментариев и структуру докстринга. Задача здесь труднее, чем в чтении: понять чужое имя проще, чем придумать своё. Зато и отдача больше — по именам в коде англоязычный коллега делает вывод об авторе за минуту, ещё до того, как дочитает функцию.
Глаголы: get, fetch, retrieve и вся семья
Словарь переводит десяток английских глаголов одинаково: «получить», «создать», «удалить». В коде они не взаимозаменяемы. Каждый несёт дополнительный смысл о стоимости и месте операции, и читатель на этот оттенок опирается, даже не замечая этого.
| Глагол | Оттенок смысла | Пример |
|---|---|---|
get | дёшево и рядом: поле, кэш, память | get_user_id() |
fetch | сходить наружу: сеть, внешний API | fetch_exchange_rates() |
retrieve | достать из хранилища, формальнее | retrieve_order(order_id) |
load | поднять в память крупное | load_config() |
read | прочитать поток или файл | read_manifest(path) |
find / lookup | искать, результата может не быть | find_user_by_email() |
Практическое правило: get_* не ходит в сеть. Функция get_prices(), которая внутри делает двухсекундный HTTP-запрос, рано или поздно окажется в цикле по тысяче товаров — потому что имя пообещало дешёвую операцию. Через полчаса кто-то будет объяснять в чате инцидентов, почему страница каталога отвечает по сорок минут. Виновата не архитектура, виноваты четыре буквы в имени. Здесь же и договорённость про исключения: get_* возвращает значение или бросает ошибку, а find_* имеет право вернуть None — «не нашли» для поиска нормальный исход.
Остальные семейства просто выучи.
| Семейство | Как различаются |
|---|---|
create / make / build / generate | create — создать сущность (часто с записью в БД); make — собрать простой объект в памяти; build — собрать сложное по частям (build_query); generate — произвести по алгоритму (generate_slug, generate_token). |
delete / remove / destroy / purge | delete — стереть насовсем (запись в БД, файл); remove — убрать из коллекции, сама сущность может выжить (remove_tag_from_post); destroy — уничтожить вместе со связями; purge — вычистить массово (purge_expired_sessions). |
check / validate / verify / ensure | check — посмотреть и вернуть ответ; validate — проверить по правилам и пожаловаться ошибкой; verify — подтвердить подлинность (подпись, токен); ensure — привести к нужному состоянию, если ещё не в нём (ensure_directory_exists). |
update / modify / patch / set | update — обновить существующее; patch — частично, только присланные поля; set — присвоить конкретное значение; modify в именах лучше не использовать, оно слишком размыто. |
handle / process / apply | handle — отреагировать на событие; apply — применить нечто к объекту (apply_discount); process — «обработать», самое пустое слово из троих, почти всегда заменяемо точным глаголом. |
send / publish / emit / dispatch | send — отправить конкретному адресату; publish — в топик/канал, кто угодно прочитает; emit — испустить событие внутри процесса; dispatch — направить нужному обработчику. |
start / launch / run / spin up | start — запустить и вернуть управление; run — выполнить до конца; launch — запустить нечто самостоятельное; spin up — разговорное «поднять» (инстанс, контейнер), уместно в переписке, но не в имени функции. |
Последняя строка таблицы про важное: у английского в коде и английского в чате разные регистры. Фразовые глаголы spin up, tear down, kick off, roll out отлично звучат в сообщении коллеге и разваливаются в идентификаторе, где нужна однозначность, а не живость.
Булевы имена: is, has, can, should, needs
Булева переменная называется так, чтобы имя читалось как утверждение — истинное или ложное. Пять префиксов закрывают почти все случаи.
is_published = True # состояние: пост опубликован
has_unread_comments = False # обладание: есть непрочитанные комментарии
can_edit = user.is_staff # разрешение: может редактировать
should_retry = attempt < 5 # решение: стоит ли повторить
needs_migration = version < CURRENT_SCHEMA_VERSION # требуется действие
Разница между can_ и should_ тонкая, но рабочая: can про возможность («технически разрешено»), should про целесообразность («по нашим правилам надо»). В авторизации это разные ветки кода: can_delete_post — проверка прав, should_notify_author — бизнес-правило. Смешаешь их в одной функции — получишь место, где юрист и продакт правят один и тот же if.
Три ошибки здесь встречаются постоянно. Первая — имя без префикса: active, valid, admin. Строку if user.admin: нельзя прочитать однозначно — это флаг или объект администратора? Вторая — отрицательное имя: is_not_valid, disable_cache. Оно неминуемо порождает if not is_not_valid:, и на этом месте спотыкается любой мозг, включая авторский. Флаг формулируется положительно, отрицание живёт там, где используется. Третья — слово flag внутри имени: email_flag не сообщает ровным счётом ничего.
Отдельная конвенция — обработчики событий. on_ описывает момент: «когда это случилось» — on_order_paid, onClick. handle_ описывает функцию, которая это разруливает: handle_order_paid. В React прижилось сочетание обоих — onClick={handleClick}, проп через on, функция через handle. Экосистемы договорились по-разному, и это нормально. Ненормально, когда по-разному договорились внутри одного проекта.
Единственное, множественное и счётчики
Английский различает число строже, чем русский код-стиль, и читатель на это опирается. user — один объект, users — коллекция. Цикл читается как предложение: for user in users:. Назовёшь коллекцию user_list — получишь for user in user_list: работает, но тип уехал в имя. Через полгода список станет множеством ради дедупликации, а имя останется и начнёт врать. Суффикс типа оправдан, только когда тип и есть смысл: user_ids (это идентификаторы, не пользователи), users_by_email (словарь, и ключ назван), user_queryset в Django, где ленивость набора — существенное свойство.
У счётчиков устойчивая триада. count — сколько штук: comment_count. total — сумма величин: total_amount, total_duration. num_ — префикс в стиле C, живёт в старых кодовых базах (num_retries); в новом коде пиши retry_count. Дальше идут четыре слова, которые путают чаще всего: size — размер (в байтах, в элементах), length — длина последовательности, amount — количество неисчисляемого (денег, объёма), quantity — количество исчисляемого (штук товара). Пара total_amount для денег и quantity для позиций заказа — фактический стандарт в e-commerce API. Запомни как есть, спорить не с кем.
Порядок слов: почему active_user, а не user_active
В английском определение стоит перед существительным: red car, active user, expired token. Идентификаторы живут по тому же правилу. Нарушение этого порядка — самая заметная примета кода, написанного русскоязычным автором: у нас слова можно расставлять свободнее, и рука сама пишет user_active.
| Плохо | Хорошо | Почему |
|---|---|---|
user_active | active_user | Прилагательное перед существительным. |
date_start, date_end | start_date, end_date | То же правило; вторая форма — общепринятая. |
count_items | item_count | Главное слово — count, оно последнее. |
list_of_posts | posts | Предлог of в имени почти всегда лишний. |
token_expired_at | expires_at | Внутри класса Token префикс избыточен. |
Одно исключение всё-таки есть — намеренная группировка по префиксу. Порядок ломают сознательно, чтобы связанные имена стояли рядом в автодополнении и в алфавитном списке настроек: CACHE_TIMEOUT_SHORT, CACHE_TIMEOUT_LONG. Это компромисс, а не незнание. Отличить одно от другого просто: компромисс применён ко всей группе целиком, а незнание — к одному имени из пяти.
Последнюю строку таблицы разберём отдельно, она про принцип. Имя читается вместе с контекстом. Поле token_expires_at внутри класса Token в коде выглядит как token.token_expires_at — заикание на ровном месте. Хорошее имя коротко именно потому, что часть смысла уже несут модуль, класс и тип аргумента. Не повторяй то, что и так сказано.
Слова-паразиты: data, info, manager, helper, util
Есть набор английских слов, которые выглядят технично и не значат ничего. Проверка простая: убери слово из имени. Смысл потерялся? Нет? Значит, слово было паразитическим.
# Плохо: слова есть, информации нет
user_data = fetch(...)
order_info = {...}
class DataManager: ...
def process_items(items): ...
def do_helper_stuff(): ...
# Хорошо: имя отвечает на вопрос «что именно»
user_profile = fetch(...)
order_summary = {...}
class OrderRepository: ...
def normalize_prices(items): ...
def rebuild_search_index(): ...
Откуда это берётся. Data и info дописывают, когда ещё не решили, что именно лежит в переменной; имя маскирует непонимание, и маскирует успешно — до первого ревью. Manager, helper, util, service, handler в имени класса означают «сюда я складываю всё, чему не нашёл места». Такой класс растёт до тысячи строк, и это не преувеличение. Process и handle в имени функции сообщают «что-то делает с этим», то есть ничего.
Однажды я потратил полчаса на функцию из двадцати строк, где рядом жили data, data2 и result. Оказалось: data — сырой ответ провайдера, data2 — он же после фильтрации по валюте, result — агрегаты для отчёта. Правильные имена — raw_payments, eur_payments, daily_totals — уместились в ту же строку и сэкономили бы эти полчаса каждому, кто открывал файл после автора. А открывали его многие: функция считала деньги.
Лечится вопросом на английском: what does it actually do? Ответ и есть имя. Не «обрабатывает элементы», а «нормализует цены». Не «менеджер данных», а «репозиторий заказов». Про utils.py отдельно: файл-свалка на раннем этапе не грех, но как только в нём больше пяти функций, они уже группируются по темам — и вместо utils честнее завести slug.py, money.py, dates.py.
Про аббревиатуры. Общепринятые в индустрии в порядке: id, url, http, db, api, json, uuid, ttl. Устоявшиеся в предметной области тоже — если SKU знает вся команда, писать stock_keeping_unit просто смешно. А вот самодельные сокращения ради экономии символов недопустимы: usr, cnt, tmp_res, calc_ordr_sm. Сэкономил три символа — потратил по секунде раздумий у каждого читателя за всё время жизни кода. Плохая сделка. И правило про регистр: в camelCase аббревиатура пишется как обычное слово (parseJsonResponse, HttpClient), иначе parseJSONHTTPResponse превращается в шараду.
Комментарии: «почему», а не «что»
Код уже говорит, что он делает, — это его работа. Комментарий нужен там, где из кода не видно почему: почему именно 300 секунд, почему обход вместо очевидного решения, какая внешняя странность нас на это толкнула.
# Плохо: пересказ строки кода
# Увеличиваем счётчик на единицу
counter += 1
# Плохо: перевод кода на английский
# Loop over all users and send them an email
for user in users:
send_email(user)
# Хорошо: объясняет причину, которой в коде не видно
# Провайдер отдаёт 429 при более чем 10 письмах в секунду,
# а батч-эндпоинта у них нет — поэтому шлём по одному с паузой.
for user in users:
send_email(user)
time.sleep(0.1)
Маркеры знать обязательно: по ним ищут грепом, их подсвечивает IDE. TODO — запланированная доработка, которая сейчас не мешает. FIXME — известный дефект, его надо чинить. NOTE — пояснение для читателя. HACK — сознательный костыль с объяснением, почему по-хорошему не вышло. XXX — «здесь опасно, разберись, прежде чем трогать». Хороший маркер несёт контекст и ссылку. Без них он через полгода становится археологическим слоем, который все боятся удалить.
# Плохо
# TODO: fix this later
# Хорошо
# TODO(PAY-482): drop this branch once all clients migrate to v2 of the
# webhook payload. Expected after the March release.
И главный принцип: комментарий часто оказывается симптомом плохого имени. Тянет пояснить, что делает функция? Сначала попробуй её переименовать. Комментарий // проверяем, что заказ можно вернуть над функцией check2() исчезает сам, стоит назвать её is_refundable(). Всё, что убирается переименованием, — это дублирование, а дублирование со временем расходится: код правят, комментарий забывают, и через год он врёт с уверенным видом. Устаревший комментарий хуже отсутствующего. Отсутствующему никто не верит.
Докстринги
Докстринг — это уже настоящий английский текст, пусть и короткий. У него свои жанровые правила, и первое из них про время и лицо глагола. Описательный стиль использует третье лицо Present Simple: Returns the parsed token, Raises ValueError if the payload is malformed. Не Will return — будущее время здесь ни к чему. Не This function returns — три лишних слова в самой заметной строке.
Дальше развилка, о которой спорят до сих пор. PEP 257 рекомендует императив: Return the parsed token — предписание, а не описание. Google-стиль и многие крупные проекты пишут описательное третье лицо: Returns the parsed token. Обе формы законны, выбирай любую. Незаконно другое: мешать их в одном проекте и писать Returning или This method is used to return.
Структура классическая: строка резюме, пустая строка, детали. Резюме обязано помещаться в одну строку и заканчиваться точкой — оно уезжает в подсказку IDE и в сгенерированную документацию, где на него смотрят полсекунды и больше никогда не возвращаются.
def parse_access_token(raw: str) -> AccessToken:
'''Return the access token decoded from an Authorization header.
Strips the Bearer prefix, verifies the signature against the currently
active public key and checks the expiry claim. The key set is cached
for five minutes, so a freshly rotated key may be rejected briefly.
Args:
raw: Raw value of the Authorization header, including the prefix.
Returns:
The decoded token with its claims.
Raises:
InvalidTokenError: If the signature does not match or the token
has already expired.
'''
Что делает этот текст хорошим. Резюме отвечает на вопрос «что я получу», а не «как оно устроено внутри». Второй абзац сообщает то, чего в сигнатуре нет: побочные действия и оговорку про кэш ключей. Вот эта оговорка — про то, что свежеротированный ключ пять минут будет отвергаться, — и есть причина, по которой докстринг вообще писали. Без неё кто-то будет отлаживать «случайные» 401 после ротации. Секции Args / Returns / Raises — стандартный набор; в NumPy-стиле это Parameters / Returns / Raises, в reStructuredText — :param:, :returns:, :raises:. Формат вторичен, набор смыслов один и тот же.
Чего в докстринге быть не должно: пересказа сигнатуры (Takes a string and returns an object — это и так видно), вводных оборотов (This function is used for parsing...), извинений (Simple helper to...) и русского языка в проекте, который претендует на англоязычный код. Проверь себя упражнением: вычеркни первые три слова своего докстринга. Смысл не пострадал? Значит, они были лишними.
Именование в API
Публичный API — это имена, которые переименовать уже нельзя: за них держатся чужие клиенты, о существовании которых ты не знаешь. Поэтому здесь конвенции жёстче всего.
GET /api/v1/orders
POST /api/v1/orders
GET /api/v1/orders/41
PATCH /api/v1/orders/41
DELETE /api/v1/orders/41
POST /api/v1/orders/41/refunds
GET /api/v1/orders?status=paid&created_after=2025-03-01
Правило первое: ресурс — существительное во множественном числе, даже когда речь про один экземпляр (/orders/41, а не /order/41). Правило второе: глагол выражается HTTP-методом, а не путём. Пути /getOrder, /createOrder, /deleteOrderById дублируют то, что уже сказано методом, — так пишут те, кто переносит в HTTP привычки из RPC. Правило третье: вложенность означает принадлежность (/orders/41/refunds — возвраты этого заказа), а фильтрация уходит в параметры запроса.
Для действий, не ложащихся на CRUD, есть два подхода: превратить действие в ресурс (POST /orders/41/refunds — создаём возврат) или честно сделать глагольный эндпоинт (POST /orders/41/cancel). Первый почти всегда лучше. У возврата есть собственная сущность, id, статус и история — и однажды всё это захочется прочитать, а читать в схеме «действие» будет нечего.
В телах JSON правила похожие: одно соглашение о регистре на весь API (snake_case или camelCase, но не оба сразу), время в полях с суффиксом _at в ISO 8601 и UTC, деньги — суммой и валютой раздельно.
{
"id": 41,
"status": "paid",
"total_amount": "129.90",
"currency": "EUR",
"created_at": "2025-03-14T10:22:03Z",
"items": [
{"sku": "TSHIRT-BLK-M", "quantity": 2, "unit_price": "64.95"}
]
}
А теперь то, что встречается в жизни: {"datas": [...], "flag": 1, "usr_nm": "tim", "dt": 1710411723, "sum": "129.90"}. Здесь несуществующее множественное число (data неисчисляемо, формы datas в английском нет), бессмысленный flag, самодельные сокращения, таймстамп без единиц — секунды это или миллисекунды, выяснится опытным путём в проде — и sum вместо amount. Последнее особенно обидно: sum по-английски означает результат сложения, а не денежную сумму как атрибут заказа. Интегратор прочитает это поле как «итог по позициям» и не угадает.
Кейс из реального проекта: рефакторинг именований
Кусок кода из сервиса заказов — по духу совершенно настоящий. Он работает, тесты зелёные, ревью прошёл. Читать его невозможно, и вся тяжесть тут исключительно языковая: логика в семь строк.
def proc(d, fl=False):
'''This function is used for processing of order data.'''
res = []
for i in d:
# проверяем что заказ оплачен
if i["st"] == 1:
# считаем сумму
s = i["price"] * i["cnt"]
if fl:
s = s * 0.9 # скидка
res.append({"id": i["id"], "sum": s})
return res
Проблемы здесь из разных категорий, поэтому по списку. proc — самодельное сокращение слова-паразита process: не говорит ни что на входе, ни что на выходе. d и fl — одна буква и «флаг», причём флаг чего, из fl=False не узнать никогда. Докстринг открывается четырьмя словами ни о чём — This function is used for — и продолжается громоздким processing of order data вместо нормального глагола. Локальные res, i, s нечитаемы, а i ещё и врёт: по традиции это индекс, здесь это элемент. Магические 1 и 0.9 объяснены комментариями-пересказами вместо того, чтобы получить имена. Ключи "sum", "st", "cnt" вместо отраслевых amount, status, quantity. И русские комментарии в англоязычном коде — смесь, которая читается хуже любого из двух языков по отдельности.
PAID = "paid"
LOYALTY_DISCOUNT_RATE = Decimal("0.10")
def summarize_paid_orders(
orders: list[dict], apply_loyalty_discount: bool = False
) -> list[OrderSummary]:
'''Return a summary with the payable amount for every paid order.
Orders in any other status are skipped. When the customer is in the
loyalty programme, the discount is applied to the line total, not to
the unit price, because tax is calculated on the discounted total.
'''
summaries = []
for order in orders:
if order["status"] != PAID:
continue
line_total = order["unit_price"] * order["quantity"]
if apply_loyalty_discount:
line_total *= 1 - LOYALTY_DISCOUNT_RATE
summaries.append(OrderSummary(id=order["id"], amount=line_total))
return summaries
Что изменилось. Имя функции стало предложением: summarize paid orders — из него сразу видны и фильтр, и результат. Аргумент apply_loyalty_discount читается прямо в месте вызова, не заглядывая в определение. Константы забрали оба магических числа, и комментарии «считаем сумму» и «скидка» испарились сами — объяснять стало нечего. Докстринг теперь говорит то, чего в коде не видно: скидка применяется к строке, а не к цене за штуку, потому что налог считается от суммы со скидкой. Ключи словаря приведены к отраслевым словам. Ранний continue вместо вложенного if — уже не про язык, но он появился сам собой, как только стало видно, что условие описывает пропуск, а не выборку. Так почти всегда и бывает: когда починишь имена, структура подтягивается следом.
Сравни две строки вызова — вопрос о пользе именования закроется сам:
rows = proc(data, True)
summaries = summarize_paid_orders(orders, apply_loyalty_discount=True)
Типичные ошибки
Транслит и русские корни в именах
Имена spisok_zakazov, proverka_dostupa, get_spisok() и гибриды вроде userSpisok живут в русскоязычных проектах прекрасно — до определённого момента. Аргумент «мы пишем только для себя» разваливается в тот день, когда в команду приходит человек, не читающий кириллицу; или когда модуль уезжает в open source; или когда ты сам через год грепаешь по orders и получаешь ноль совпадений, хотя код про заказы. Транслит не ищется, не переводится и не подсказывается автодополнением: proverka не значит ничего ни на одном языке мира. Худший подвид — смесь внутри одного идентификатора: send_pismo, calc_stoimost. Читатель переключает языковой контекст посреди слова, и это утомляет сильнее, чем кажется. Лечение занимает тридцать секунд: открой словарь. «Накладная» — invoice, «остаток» — balance или stock, «черновик» — draft, «зачисление» — credit. Эти слова всё равно понадобятся, когда дойдёт до документации платёжного провайдера.
actual вместо current, ложные друзья переводчика
Английское actual означает «фактический, реальный» — в противоположность заявленному или ожидаемому. Не «актуальный» и не «текущий». Носитель прочитает actual_price как «цена, которая получилась на самом деле», и будет искать, с чем её сравнивают. Текущая цена — это current_price. Уместно actual ровно в одном контексте: в паре с expected при сравнении в тестах. Другие постоянные ловушки: accurate — точный, а не «аккуратный»; list — список, а не «лист» (страница — page, лист бумаги — sheet); magazine — журнал для чтения, а склад — warehouse; data — неисчисляемое, форм datas не существует; information тоже неисчисляемое, informations — грубая ошибка; decade — десятилетие, а не декада; fabric — ткань, а фабрика — factory. И одна не-ложная, но частая: controlling в русском смысле «проверяющий» — по-английски проверка это check или audit.
Отрицательные булевы имена
Имя is_not_valid или disable_notifications выглядит безобидно ровно до того момента, как попадёт в условие. if not is_not_valid: — двойное отрицание, на котором спотыкается любой читатель, включая автора через месяц. Комбинируются такие флаги ещё хуже: if not disable_cache and not is_not_expired: невозможно прочитать вслух не запутавшись. Правило одно: флаг формулируется положительно, отрицание ставится в месте использования. is_valid, notifications_enabled, cache_enabled. Особенно это касается настроек. Строка DISABLE_X = False в конфиге требует двух мыслительных операций вместо одной — и именно на таких строках в три часа ночи кто-нибудь выключает не то, что собирался. X_ENABLED = True читается сразу.
Комментарий-перевод и докстринг «This function is used for...»
Две ошибки одного корня: текст написан не для читателя, а для галочки. Комментарий # increment the counter над строкой counter += 1 переводит код на английский слово в слово, не добавляя ни бита информации, зато создавая вторую копию правды — и эта копия обязательно разойдётся с первой. Докстринг, начинающийся с This function is used for, This method allows you to или Simple helper that, тратит самую заметную строку на слова, верные для любой функции на свете. Тест на пустоту: закрой код, прочитай только комментарий. Ничего нового? Его надо не улучшать, а удалять. А если выяснилось, что объяснять придётся много, дело обычно не в комментарии — дело в имени или в размере функции.
Итог
Имена в коде — письменная английская речь с очень высокой ценой ошибки: её читают ежедневно и годами. Теперь ты знаешь, что fetch обещает поход в сеть, а ensure — приведение к нужному состоянию. Умеешь строить булевы имена на пяти префиксах и не выворачивать их наизнанку отрицанием. Помнишь, что определение стоит перед существительным, что data, info и manager чаще всего маскируют непонимание, а комментарий обязан объяснять «почему» — иначе он лишний и вреден. Докстринг у тебя состоит из резюме, деталей и стандартных секций. API-имена следуют отраслевым конвенциям, а не привычкам автора.
В следующей главе — ещё один английский текст, который ты пишешь каждый день и который останется в проекте навсегда: сообщения коммитов. Императив и правило проверки «If applied, this commit will...», форма 50/72, Conventional Commits и её связь с SemVer, и главное — как нарезать работу так, чтобы через год по истории можно было понять, что здесь вообще происходило.