Хотя одним из постулатов 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 и явных приведений типов

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

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

  1. Обработка исключений через pass.
  2. Обработка всех встретившихся в блоке эксепшенов одним 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