Хотя одним из постулатов Python way является фраза "There should be one - and preferably only one - obvious way to do it" - в действительности это не совсем так. В языке достаточно много способов сделать что-то очевидным для новичка способом, но выглядящим ужасно для опытного питониста (и наоборот). Самое ужасное, что, несмотря на все старания Гвидо и компании, такой способ может быть даже и не один.
На Python, как и на любом языке, можно писать или идиоматично, или плохо. Конечно, даже написанный в полном соответствии с best practices может реализовывать запутанный алгоритм, что тоже не очень хорошо. Но, если отвлечься от грустного, что может быть лучше хорошего алгоритма, правильно и идиоматично реализованного?
Теперь о том, как нужно писать, а как нет.
Индексы
В Python все стандартные структуры данных являются итераторами и не требуют явных указаний индексов. Но вначале старые привычки часто берут верх и побуждают писать что-то вроде:
for i in xrange(len(values)):
print values[i]
На самом деле, индекс здесь - это лишняя сущность, которая не несет никакого смысла. Правильно написать так:
for value in values:
print value
Но получить индекс элемента в последовательности можно и более элегантным способом (если это действительно нужно):
for position, student in enumerate(students):
print position, student
Но такое бывает нужно чуть чаще, чем никогда.
Если кратко: если можно обойтись без индексов - нужно без них обойтись.
Циклы к месту и не очень
У пришедших с других языков (С, С++, Perl, Java) остается привычка использовать циклы везде, где требуется обход коллекции, хотя в Питоне их в большинстве случаев можно заменить другими инструментами: выражениями-генераторами или функциями высшего порядка.
Банальная задача: нужно выбрать из списка пользователей, имеющие свойство is_paid, установленное в True. Плохое решение:
paid_users = []
for user in users:
if user.is_paid:
paid_users.append(user)
Как это написать правильно:
paid_users = [user for user in users if user.is_paid]
Теперь у нас нет лишних уровней вложенности и код читается как одна фраза, а не как последовательность действий. Кроме того, можно возвращать не список, а генератор (сделать вычисление ленивым), всего лишь поменяв квадратные скобочки на круглые:
paid_users = (user for user in users if user.is_paid)
Если все это кажется странным и непонятным - самое время вернуться к чтению Луца:)
Теперь о функциях высшего порядка. Допустим, у нас есть простая задача: существует переменная records, представляющая собой итератор по объектам, имеющим свойство value. Нужно подсчитать сумму значений в этом поле у всех объектов.
Как часто пишут:
count = 0
for record in records:
count += record.value
Три строчки, которые и не пахнут декларативностью. А как нужно было это написать:
count = sum((record.value for record in records))
Если нам нужно выполнить над списком какую-то другую функцию - осторожно использовать reduce. Осторожно потому, что reduce в некоторых случаях работает не так быстро, как хотелось бы (об этом будет ниже).
count = reduce(operator.mul, (record.value for record in records))
А еще можно использовать функции map и filter как альтернативу выражениям-генераторам. Но это уже на любителя.
Если кратко: не стоит использовать циклы там, где можно использовать более высокоуровневые инструменты, вроде функций для работы со списками, функций высшего порядка (map, filter, reduce) или выражений-генераторов.
Велосипеды вместо стандартных средств языка
Для примера можно взять крайне банальную операцию конкатенации строк.
Когда человек проникается функциональной парадигмой, его руки тянуться засунуть некоторые приемы туда, куда их засовывать в конкретном языке более чем не стоит. Например, в ту же конкатенация строк.
file_path = reduce(lambda x, y: x + '/' + y, file_path)
Если посмотреть, что тут творится: на каждом шаге создается новая строка, которая бы объединяла все предыдущие и дополняла ее новым фрагментом пути - как следствие, память утекает тоннами.
К слову, некоторые из тех, чей мозг девственно чист по отношению ко всякой функциональщине, пишут ничуть не лучше (пример из реального кода):
new_file_path = ''
for path_part in file_path:
new_file_path = path + new_file_path
Тот же reduce, только реализованный руками, со всеми его недостатками, лишней сущностью path_part и дополнительным уровнем вложенности. А вот и более правильный способ:
file_path = '/'.join(file_path)
Работает быстро и требует всего лишь линейную память для работы.
В Python есть достаточно много встроенных функций, которые облегчают жизнь. Например, не знакомый со встроенными функциями может написать вот такой замечательный код:
min_el = None
for record in records:
if record.value < min_el:
min_el = record.value
Хотя можно было бы написать банальное и более быстрое решение (оно действительно работает быстрее за счет того, что sum на самом деле - сишный биндинг):
min_el = min((record.value for record in records))
Вернемся к предыдущему примеру про склейку пути к файлу. Код из этого примера привязан к UNIX-платформе из-за используемых слэшей '/'. В реальном коде, конечно же, правильнее использовать встроенную платформонезависимую функцию из модуля os:
import os
file_path = os.path.join(file_path)
Если кратко: не стоит изобретать лишний раз свои велосипеды там, где в языке уже есть готовое решение. А решений предостаточно:
- Функции min и max.
- Множества (самый простой способ проуникалить элементы в неотсортированном списке, помимо всего прочего).
- Функции any и all.
- Интересные типы данных в модуле collections. Особое внимание следует обратить на defaultdict и namedtuple.
- У каждого стандартного типа данных есть много методов, которые могут сильно облегчить жизнь.
- Вся стандартная библиотека.
Если кратко: если можно использовать стандартные средства языка - нужно их использовать.
Странные выражения в if
Люди, пришедшие с других ЯП временами, пишут что-то подобное:
if is_admin == True:
print 'Hello, admin'
Кто-то, читавший про оператор is, может написать и так:
if is_admin is True:
print 'Hello, admin'
Или еще интереснее для структур данных:
# type(values) == list
if values != []:
print values
Кто-то может запомнить, что выражение в if не нужно явно сравнивать с True или False, но забыть, что значение выражения самое неявно приводится к типу bool:
if bool(values):
print values
Вышеуказанные примеры нужно переписать так:
if is_admin:
print 'Hello, admin'
if values:
print values
Правила приведения к bool просты и описаны в стандартной документации. Если вкратце, ложь при приведении типов возвращают:
- None
- False
- Число 0 в любых его формах (0, 0.0, 0j).
- Пустые стандартные структуры данных ({}, (), '', []).
- Экземпляры пользовательских классов, в которых определены методы bool() или len(), и если эти методы возвращают 0 или False.
Если кратко: инструкцию if нужно писать как if <выражение>:
без всяких сравнений с True или False и явных приведений типов
Неправильная обработка исключений
На самом деле, такую проблему я видел не только в Питоне, но именно в нем она иногда играет новыми красками. Проблемы, на самом деле, две:
- Обработка исключений через pass.
- Обработка всех встретившихся в блоке эксепшенов одним except.
Использование pass
Беда не только Python, но и других языков. Иногда можно видеть что-то вроде:
parsed_json = {}
try:
parsed_json = json.loads(raw_json)
<много кода>
except:
pass
Чем это плохо? Исключение - ситуация нештатная, которая должна всегда явно обрабатываться. Способ обработки зависит от особенностей кода: какие-то моменты нужно логировать, какие-то выводить пользователю в виде ошибки, в каких-то нужно просто падать. Но это тема для отдельного поста.
Чем же плох вышеприведенный подход? Есть риск очень долго копаться чтобы узнать, что же в приложении пошло не так. Даже больше - определить, что что-то пошло не так можно только по косвенным признакам, которые сообщают только факт ошибки (в лучшем случае), но ничего не говорят о месте и причине ее возникновения. Этот пример будет стабильно выдавать пустой словарь в качестве значения переменной parsed_json вне зависимости от того, какой json мог бы лежать в переменной raw_json (если представить, что где-то выше она объявляется).
Самое интересное начинается, если не указать, какие именно ошибки нужно обработать.
Обработка всех исключений в блоке
Если вставить в интерпретатор пример кода из раздела выше - он отработает без ошибок. Хотя по-хорошему то не должен: не импортирован модуль json, а строка <много кода>
- вообще синтаксическая ошибка. Это и есть то самое комбо, которое приводит ко многим бездарно потраченным за отладкой часам. Проблем можно было бы избежать, если бы перехватывались только нужные исключения:
parsed_json = {}
try:
parsed_json = json.loads(raw_json)
<много кода>
except (ValueError, TypeError):
print 'Incorrect json'
Теперь исключения NameError и SyntaxError будут успешно отловлены, а ValueError и TypeError успешно обработаны. Хочется обратить внимание, что SyntaxError является обычным исключением - интерпретатор CPython проверяет корректность синтаксиса только во время исполнения.
Краткий вывод: всегда нужно указывать явно как и какие исключения нужно обработать.
Вторая часть тут.
Comments
comments powered by Disqus