Как не нужно писать на 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 и явных приведений типов
Неправильная обработка исключенийНа самом деле, такую проблему я видел не только в Питоне, но именно в нем она иногда играет новыми красками. Проблемы, на самом деле, две:
- Обработка исключений через pass.
- Обработка всех встретившихся в блоке эксепшенов одним except.
Беда не только Python, но и других языков. Иногда можно видеть что-то вроде:
Чем это плохо? Исключение - ситуация нештатная, которая должна всегда явно обрабатываться. Способ обработки зависит от особенностей кода: какие-то моменты нужно логировать, какие-то выводить пользователю в виде ошибки, в каких-то нужно просто падать. Но это тема для отдельного поста.
Чем же плох вышеприведенный подход? Есть риск очень долго копаться чтобы узнать, что же в приложении пошло не так. Даже больше - определить, что что-то пошло не так можно только по косвенным признакам, которые сообщают только факт ошибки (в лучшем случае), но ничего не говорят о месте и причине ее возникновения. Этот пример будет стабильно выдавать пустой словарь в качестве значения переменной parsed_json вне зависимости от того, какой json мог бы лежать в переменной raw_json (если представить, что где-то выше она объявляется).
Самое интересное начинается, если не указать, какие именно ошибки нужно обработать.
Обработка всех исключений в блокеЕсли вставить в интерпретатор пример кода из раздела выше - он отработает без ошибок. Хотя по-хорошему то не должен: не импортирован модуль json, а строка <много кода> - вообще синтаксическая ошибка. Это и есть то самое комбо, которое приводит ко многим бездарно потраченным за отладкой часам. Проблем можно было бы избежать, если бы перехватывались только нужные исключения:
Теперь исключения NameError и SyntaxError будут успешно отловлены, а ValueError и TypeError успешно обработаны. Хочется обратить внимание, что SyntaxError является обычным исключением - интерпретатор CPython проверяет корректность синтаксиса только во время исполнения.
Краткий вывод: всегда нужно указывать явно как и какие исключения нужно обработать.