Полезные практики
Методом проб и ошибок выработал набор практик, с которыми работаю лучше. Стал допускать меньше ошибок, легче отслеживаю бизнес-логику, быстрее вношу изменения в код. Эти практики не следуют строго определенным парадигмам. Наверняка под каждую придумали паттерн, но я об этом ничего не знаю и расскажу простыми словами.
Первое. Указываю тип переменной в имени
Задаю переменным имена по правилу <entity>_<type>
. Открыв код месячной
давности, сразу вижу, где и какие типы. Даже самая коммерческая ИДЕ порой не
может понять, с чем имеет дело. А с моим правилом именования работать одно
удовольствие.
Применяю его не слепо, а выборочно. Скалярным типам, например, строкам и числам,
не указываю тип, если он ясен из контекста. Выражения name_str = 'test'
или
age_int = 42
избыточны, поскольку имя и возраст вряд ли могут быть чем-то
отличным от строки и целого.
Я добавляю в конце тип, если он неочевиден из контекста. Предположим, из ответа
чужой апихи пришло поле permission
. Что это – строковое имя, числовой код,
булево – понять со стороны невозможно. Все, что я могу – слазить в
документацию или промотать в другое место, чтобы увидеть, что с этим полем
делают дальше.
permisson = response['data']['item']['permisson']
# wtf is permisson?
А ведь достаточно назвать переменную perm_int
, и все станет ясно – это же
числовой код!
Указывать тип стоит везде, где кроется неожиданность. Айдишка объекта может быть
передана строкой, поэтому назову переменную user_id_str
, а дальше преобразую в
инт. Поле может называться item
, а внутри – гуид сущности, а не словарь. И
так далее.
Коллекциям задаю тип без исключений. В Питоне достаточно много разных коллекций. Чаще всего нас интересует только итерация, но шаг в сторону, и программа падает.
Примеры? Хотели список, чтобы изменять элементы, а пришел кортеж. Итерация по множеству проходит в разном порядке. Хеш от списка вызывает исключение. Пройтись по итератору можно только один раз. И так далее.
Если работаю со словарем с данными о пользователе, называю user_dict
. Список
пользователей – user_list
, множество – ..._set
и так далее, принцип,
думаю, понятен. Для кортежей и итераторов окончания tupl
, iter
.
Отдельно стоит упомянуть тип Queryset
, с которым постоянно работаешь в
Джанге. Обозначаю его как qs
. С этим типом сплошная беда. Он всеми силами
мимикрирует под список и кидает в неподходящий момент.
Смотрит коллега в монитор и не понимает, отчего Pytest падает и выводит следующее:
assert [1L, 2L, 3L] == [1, 2, 3]
>>> long trace...
Потому что справа список, а слева – квери-сет. Он выводится как список, но не равен ему.
Отдельным абзацем замечу, что не приемлю лексемы data
в именах
переменных. user_data
, response_data
– ужасные имена. Любая переменная,
даже ноль, уже несет данные. Понятней не стало. Это словарь, список или что?
Добавлять на конце s
тоже нет смысла. Коллекция подразумевает больше одного
элемента. Если не указан тип, я опять в беде: users
– это сет, словарь или
кортеж? Можно ли брать слайс? Подставить в ключ словаря?
Падение на None
(он же nil, null, undefined
, etc) – особая история. В
программировании до сих пор нет понимания, что делать с пустыми типами. Чтобы
обезопасить код, полезно явно задать имя вроде user_or_none
или, для
краткости, user_none
. Это вынудит программиста выполнить проверку перед тем,
как что-то делать с данными.
Второе. Избегаю циклов
О вреде циклов я уже писал, и не раз. Если коротко, то:
- ручное накопление списка или словаря чревато багами
- со временем цикл разрастается, обрастает вложенными
for, if
- из-за отступов плохо видно бизнес-логику
- цикл поощрает плохую практику – впендюрить
continue
вместо того, чтобы отфильтровать данные до входа в цикл - цикл плохо поддается рефакторингу, поскольку затягивает контекст – коллекцию-результат, локальные переменные, вложенные циклы.
Решение – использовать функции высшего порядка map
, filter
. Я отрицательно
отношусь к трехэтажным лямбдам. Использую обычные функции, объявленные через
def/func/defn
.
def get_item_list(user_id):
def get_item(product):
...
item_qs = models.Item.objects.filter(user_id=user_id)
return map(get_item, item_qs)
Я просто объявляю функцию в том месте, где она нужна, и не парюсь за производительность или дзен. Код становится на рельсы: коллекция –> фильтрация –> действие –> другая коллекция –> свертка. Появляется ощущение структуры программы, приходит упорядоченность.
Добавить новое бизнес-правило в такой код очень легко. Это будет или еще один фильтр, или изменится действие над элементом. В любом случае не съедет весь код, как в примере ниже:
items = users.get_items()
res = []
for item in items:
if items.color = 'red':
continue
res.append(item.id)
Приходит эффективный менеджер и говорит, что теперь операция должна выполняться по всем юзерам. Не вопрос, отвечает программист и тупо сдвигает табом:
res = []
users = get_all_users()
for users in users:
items = users.get_items()
for item in items:
if items.color = 'red':
continue
res.append(item.id)
Дифф покажет полную замену кода. Добавить сюда еще пару вложенных условий, перехват ошибок, запись в лог – и код останется выкинуть на помойку.
Простое правило “дейтвие, коллекция, мап, свертка” работает без нареканий и легко адаптируется под новые требования.
Третье. Отлавливаю ошибки как можно раньше
Почти любая операция небезопасна и может кинуть исключение. Проблема в том, что
одновременно писать бизнес-логику и следить за ошибками трудно. Каждая ошибка –
это блок try-catch
и запись в лог, за которыми не видно главную мысль.
Исключения уже вовсе не означают исключительную ситуацию. Они стали
сигналами. Тот же Питон кидает и сам отлавливает определенные исключения в ходе
работы. Эта практика перешла и в бизнес-логику. Например, когда нет прав на
операцию, выбрасывают исключение PermissionError
. Обработчик сверху ловит его
и выводит адекватный результат.
Мне не нравится эта ситуация, потому что она ненадержна. Язык не может внятно сказать, какие исключения возникают при конкретной операции. Это может быть описано в документации, но чаще всего выясняется эмпирически.
Не отлавливать свои же исключения неправильно с этической точки зрения. Ты словно говоришь коллегам – вот написал код, но меня не волнуют ошибки. Да, упадет, если в ответе нет ключа. Но ты оберни и залогируй. Превозмогай, это не мои проблемы.
Заворачивать весь код в try-catch
– не выход. Поможет возврат пары, как в
Golang. С небольшим отличием – не (ok, result_or_error)
, как принято в
последнем, а (err_or_null, result_or_null)
, как в Node.js. Второй вариант
логическии правильней.
Заворачиваю функцию в простой декоратор:
def err_result(f):
def wrapper(*args, **kwargs):
try:
return None, f(*args, **kwargs)
except Exception as e:
return e, None
return wrapper
Или вызываю just-in-place:
def some_func(foo):
...
err, result = err_result(some_func)(42)
Вариант с мапом. Функция-обработчик раскладывает пару на составные части с помощью деструктивного синтаксиса:
item_queryset = models.Item.filter(...)
def process_item(item):
...
pair_list = map(err_result(process_item), item_queryset)
def process_pair((err, result)):
if result:
# positive brunch
if err:
# negative brunch
map(process_pair, pair_list)
Или отделяю котлеты от мух: разбиваю список пар на плоские списки ошибок и результатов. Отдельно логирую ошибки. Передаю результаты на дальнейшую обработку. Так в коде появляется порядок.
Конечно, в случае с одноразовым скриптом я могу завернуть все в глобальный
try-catch
. Но прекрасно отдаю себе отчет в том, какие последствия это имеет в
боевом коде.
Заключение
Вот такие принципы я проповедую в текущем проекте. С ними стало работать легче. Меньше падений на типах, внезапных трейсов.
Повторюсь, описанные принципы не идеальны, но с ними возникает чувство порядка. Словно код становится на рельсы, а вместе с ним и процесс. Возникает линейность, предсказуемость действий.
Кто-то скажет, что это не питоник-вэй, что диктатор не велел. Но кому это интересно? Мы пишем код не для Гвидо или Торвальдса, а для начальников, которые в гробу видали все паттерны, главное, чтобы код работал.
Допускаю, что прочту этот пост через 2 года и подумаю, каким чудаком я был, но пока что так.
Нашли ошибку? Выделите мышкой и нажмите Ctrl/⌘+Enter
Vadim, 17th May 2016, link
Хм, интересно. Но что если во втором и третьем
примерах(во втором пункте) будет юзеров тысяч 20, не пробегать же по списку дважды, фильтрацией и мапом. Континью не нравится, но можно ли написать в таких случаях более красиво?
Ivan Grishaev, 18th May 2016, link , parent
Насчет 20 тысяч. Если это разовый скрипт, число пробегов не важно.
Если речь идет о веб-приложении, 20 тыс. записей на запрос -- слишком много, клиент не сможет их отрендерить. Поэтому отдают пачками по 100 записей. Вы бы хотели видеть в личном кабинете Билайна 20 тысяч последних операций?
Кроме того, map и filter ленивы (в третьем питоне, аналоги imap/ifilter во втором), так что проход будет один.
Vadim, 18th May 2016, link , parent
Круто, про imap и ifilter не знал, спасибо. Конкретно про 20 тысяч (в реальности на порядок больше получается) это я описывал ситуацию когда есть куча неких скриптов которые мигрируют данные из одной системы в другую.