Как не нужно писать на Python, часть 1

Как не нужно писать на Python, часть 1

Хотя одним из постулатов Python way является фраза "There should be one - and preferably only one - obvious way to do it" - в действительности это не совсем так. В языке достаточно много способов сделать что-то очевидным для новичка способом, но выглядящим ужасно для опытного питониста (и наоборот). Самое ужасное, что, несмотря на все старания Гвидо и компании, такой способ может быть даже и не один.

На Python, как и на любом языке, можно писать или идиоматично, или плохо. Конечно, даже написанный в полном соответствии с best practices может реализовывать запутанный алгоритм, что тоже не очень хорошо. Но, если отвлечься от грустного, что может быть лучше хорошего алгоритма, правильно и идиоматично реализованного?

Теперь о том, как нужно писать, а как нет.

Индексы

В Python все стандартные структуры данных являются итераторами и не требуют явных указаний индексов. Но вначале старые привычки часто берут верх и побуждают писать что-то вроде:

На самом деле, индекс здесь - это лишняя сущность, которая не несет никакого смысла. Правильно написать так:

Но получить индекс элемента в последовательности можно и более элегантным способом (если это действительно нужно):

Но такое бывает нужно чуть чаще, чем никогда.

Если кратко: если можно обойтись без индексов - нужно без них обойтись.

Циклы к месту и не очень

У пришедших с других языков (С, С++, Perl, Java) остается привычка использовать циклы везде, где требуется обход коллекции, хотя в Питоне их в большинстве случаев можно заменить другими инструментами: выражениями-генераторами или функциями высшего порядка.

Банальная задача: нужно выбрать из списка пользователей, имеющие свойство is_paid, установленное в True. Плохое решение:

Как это написать правильно:

Теперь у нас нет лишних уровней вложенности и код читается как одна фраза, а не как последовательность действий. Кроме того, можно возвращать не список, а генератор (сделать вычисление ленивым), всего лишь поменяв квадратные скобочки на круглые:

Если все это кажется странным и непонятным - самое время вернуться к чтению Луца:)

Теперь о функциях высшего порядка. Допустим, у нас есть простая задача: существует переменная records, представляющая собой итератор по объектам, имеющим свойство value. Нужно подсчитать сумму значений в этом поле у всех объектов.

Как часто пишут:

Три строчки, которые и не пахнут декларативностью. А как нужно было это написать:

Если нам нужно выполнить над списком какую-то другую функцию - осторожно использовать reduce. Осторожно потому, что reduce в некоторых случаях работает не так быстро, как хотелось бы (об этом будет ниже).

А еще можно использовать функции map и filter как альтернативу выражениям-генераторам. Но это уже на любителя.

Если кратко: не стоит использовать циклы там, где можно использовать более высокоуровневые инструменты, вроде функций для работы со списками, функций высшего порядка (map, filter, reduce) или выражений-генераторов.

Велосипеды вместо стандартных средств языка

Для примера можно взять крайне банальную операцию конкатенации строк.

Когда человек проникается функциональной парадигмой, его руки тянуться засунуть некоторые приемы туда, куда их засовывать в конкретном языке более чем не стоит. Например, в ту же конкатенация строк.

Если посмотреть, что тут творится: на каждом шаге создается новая строка, которая бы объединяла все предыдущие и дополняла ее новым фрагментом пути - как следствие, память утекает тоннами.

К слову, некоторые из тех, чей мозг девственно чист по отношению ко всякой функциональщине, пишут ничуть не лучше (пример из реального кода):

Тот же reduce, только реализованный руками, со всеми его недостатками, лишней сущностью path_part и дополнительным уровнем вложенности. А вот и более правильный способ:

Работает быстро и требует всего лишь линейную память для работы.

В Python есть достаточно много встроенных функций, которые облегчают жизнь. Например, не знакомый со встроенными функциями может написать вот такой замечательный код:

Хотя можно было бы написать банальное и более быстрое решение (оно действительно работает быстрее за счет того, что sum на самом деле - сишный биндинг):

Вернемся к предыдущему примеру про склейку пути к файлу. Код из этого примера привязан к UNIX-платформе из-за используемых слэшей '/'. В реальном коде, конечно же, правильнее использовать встроенную платформонезависимую функцию из модуля os:

Если кратко: не стоит изобретать лишний раз свои велосипеды там, где в языке уже есть готовое решение. А решений предостаточно:

  • Функции min и max.
  • Множества (самый простой способ проуникалить элементы в неотсортированном списке, помимо всего прочего).
  • Функции any и all.
  • Интересные типы данных в модуле collections. Особое внимание следует обратить на defaultdict и namedtuple.
  • У каждого стандартного типа данных есть много методов, которые могут сильно облегчить жизнь. .

Если кратко: если можно использовать стандартные средства языка - нужно их использовать.

Странные выражения в if

Люди, пришедшие с других ЯП временами, пишут что-то подобное:

Кто-то, читавший про оператор is, может написать и так:

Или еще интереснее для структур данных:

Кто-то может запомнить, что выражение в if не нужно явно сравнивать с True или False, но забыть, что значение выражения самое неявно приводится к типу bool:

Вышеуказанные примеры нужно переписать так:

Правила приведения к bool просты и описаны в стандартной документации. Если вкратце, ложь при приведении типов возвращают:

  • None
  • False
  • Число 0 в любых его формах (0, 0.0, 0j).
  • Пустые стандартные структуры данных (, (), '', []).
  • Экземпляры пользовательских классов, в которых определены методы bool() или len(), и если эти методы возвращают 0 или False.

Если кратко: инструкцию if нужно писать как if <выражение>: без всяких сравнений с True или False и явных приведений типов

Неправильная обработка исключений

На самом деле, такую проблему я видел не только в Питоне, но именно в нем она иногда играет новыми красками. Проблемы, на самом деле, две:

  1. Обработка исключений через pass.
  2. Обработка всех встретившихся в блоке эксепшенов одним except.
Использование pass

Беда не только Python, но и других языков. Иногда можно видеть что-то вроде:

Чем это плохо? Исключение - ситуация нештатная, которая должна всегда явно обрабатываться. Способ обработки зависит от особенностей кода: какие-то моменты нужно логировать, какие-то выводить пользователю в виде ошибки, в каких-то нужно просто падать. Но это тема для отдельного поста.

Чем же плох вышеприведенный подход? Есть риск очень долго копаться чтобы узнать, что же в приложении пошло не так. Даже больше - определить, что что-то пошло не так можно только по косвенным признакам, которые сообщают только факт ошибки (в лучшем случае), но ничего не говорят о месте и причине ее возникновения. Этот пример будет стабильно выдавать пустой словарь в качестве значения переменной parsed_json вне зависимости от того, какой json мог бы лежать в переменной raw_json (если представить, что где-то выше она объявляется).

Самое интересное начинается, если не указать, какие именно ошибки нужно обработать.

Обработка всех исключений в блоке

Если вставить в интерпретатор пример кода из раздела выше - он отработает без ошибок. Хотя по-хорошему то не должен: не импортирован модуль json, а строка <много кода> - вообще синтаксическая ошибка. Это и есть то самое комбо, которое приводит ко многим бездарно потраченным за отладкой часам. Проблем можно было бы избежать, если бы перехватывались только нужные исключения:

Теперь исключения NameError и SyntaxError будут успешно отловлены, а ValueError и TypeError успешно обработаны. Хочется обратить внимание, что SyntaxError является обычным исключением - интерпретатор CPython проверяет корректность синтаксиса только во время исполнения.

Краткий вывод: всегда нужно указывать явно как и какие исключения нужно обработать.

📎📎📎📎📎📎📎📎📎📎