Методом проб и ошибок выработал набор практик, с которыми работаю лучше. Стал допускать меньше ошибок, легче отслеживаю бизнес-логику, быстрее вношу изменения в код. Эти практики не следуют строго определенным парадигмам. Наверняка под каждую придумали паттерн, но я об этом ничего не знаю и расскажу простыми словами.

Первое. Указываю тип переменной в имени

Задаю переменным имена по правилу <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 года и подумаю, каким чудаком я был, но пока что так.