Как стать автором
Обновить

Code Like a Pythonista: Idiomatic Python (part2)

Время на прочтение11 мин
Количество просмотров18K
Автор оригинала: David Goodger
Kaa, the Python


После небольшого перерыва представляю заключительную часть перевода статьи Дэвида Гуджера «Пиши код, как настоящий Питонист: идиоматика Python»


Ссылки на первую и вторую части.


Еще раз подчеркну, автор в этой статье не открывает Америку, большинство Питонистов не найдут в ней какой-то «особой магии». Но довольно подробно перечисляются методологии использования и выбора различных конструкций в Python с точки зрения удобочитаемости и близости к идеологии PEP8.
В некоторых местах в авторской статье отсутствуют примеры исходных кодов. Разумеется, оставил как есть, придумывать свои не стал, в принципе должно быть понятно, что имел в виду автор.





Генераторы списков («List Comprehensions» — возможно, как «свертка списков» — примеч. перев.)


Генераторы списков («listcomps» для краткости) — синтаксическое сокращение для следующего паттерна:
Традиционный путь, с for и if операторами:
new_list = []<br/>
for item in a_list:<br/>
    if condition(item):<br/>
        new_list.append(fn(item))<br/>

А так с генератором списка:
new_list = [fn(item) for item in a_list<br/>
            if condition(item)]<br/>

Генераторы списков понятны и кратки вплоть до точки. Вам может понадобиться множество вложенных for и if-условий в генераторе списка, но для двух, трех циклов, или наборов if-условий, я рекомендую использовать вложенные циклы for. В соответствии с Дзеном Python, лучше выбрать более читабельный способ.
Например, список квадратов числового ряда 0–9:
>>> [n ** 2 for n in range(10)]<br/>
[0, 149162536496481]<br/>

Список квадратов нечетных между 0–9:
>>> [n ** 2 for n in range(10if n % 2]<br/>
[19254981]<br/>



Выражения-генераторы (1)


Давайте просуммируем квадраты чисел до 100:
В цикле:
total = 
for num in range(1101):<br/>
    total += num * num <br/>

Вы можете использовать функцию sum чтобы быстро собрать подходящую нам последовательность.
С генератором списка:
total = sum([num * num for num in range(1101)])<br/>

С выражением-генератором:
total = sum(num * num for num in xrange(1101))<br/>

Выражения-генераторы («genexps») — просты, как генераторы списков, но генераторы списков «жадные», а выражения-генераторы — «ленивые». Генератор списка вычисляет целый список-результат, весь сразу. Выражение-генератор вычисляет только одно значение за проход, когда понадобилось. Это особенно полезно для длинных последовательностей, когда вычисляемый список — просто промежуточный шаг, а не окончательный результат.
В этом случае нам интересна только итоговая сумма; нам не нужен промежуточный список из квадратов чисел. Мы используем xrange по той же причине: она возвращает значения лениво, по одному за итерацию.

Выражения-генераторы (2)


Например, если бы мы суммировали квадраты нескольких миллиардов чисел, мы бы столкнулись с нехваткой памяти, а выражения-генераторы такой проблемы не имеют. Но это, все-же, требует время!
total = sum(num * num<br/>
            for num in xrange(11000000000))<br/>

Различие синтаксиса в том, что генератор списка помещается в квадратные скобки, а выражение-генератор нет. Выражения-генераторы иногда требуется заключить в круглые скобки, так что вы должны всегда их использовать.
Основное правило:
  • Используйте генератор списка, когда вычисляемый список является требуемым результатом.
  • Используйте выражение-генератор, когда вычисляемый список — всего лишь промежуточный шаг.


Вот пример, который попался недавно в работе.
?(тут почему-то код примера отсутствует — прим. перев.)
Нам нужен словарь, содержащий номера (как строками, так и целыми) и коды месяцев для будущих контрактов. Он может быть получен всего одной строкой кода.
?(тут почему-то код примера отсутствует — прим. перев.)
Нам поможет следующее:
  • dict() принимает список из пар ключ/значение (2-кортежи).
  • У нас есть список кодов месяцев (каждый месяц закодирован одним символом-буквой, а строка также является списком символов). Мы применяем функцию enumerate для этого списка, чтобы получить пронумерованные коды всех месяцев.
  • Номера месяцев начинаются с 1, но Python начинает индексацию с 0, так что номер месяца на единицу больше соответствующего индекса.
  • Нам нужен поиск месяца по строке и номеру. Мы можем использовать для этого функции int(), str() и перебор их в цикле.


Недавний пример:
month_codes = dict((fn(i+1), code)<br/>
    for i, code in enumerate('FGHJKMNQUVXZ')<br/>
    for fn in (intstr))<br/>

month_codes результат:
1:  'F',  2:  'G',  3:  'H',  4:  'J'...<br/>
 '1''F''2''G''3''H''4''J'...}<br/>


Сортировка


Сортировать списки в Python легко:
a_list.sort()<br/>

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

Мы можем использовать встроенный метод списка sort со специальной функцией:
def custom_cmp(item1, item2):<br/>
    return cmp((item1[1], item1[3]),<br/>
               (item2[1], item2[3]))<br/>
<br/>
a_list.sort(custom_cmp)<br/>

Это работает, но чрезвычайно тормозит с большими списками.

Сортировка с DSU *


DSU = Decorate-Sort-Undecorate
* Примечание: DSU чаще не столь необходим. Смотри в следующем разделе «Сортировка с ключом» описание другого метода.
Вместо создания специальной функции сравнения, мы создаем вспомогательный список, с помощью которого сортировка будет обычной:
# Decorate:<br/>
to_sort = [(item[1], item[3], item)<br/>
           for item in a_list]<br/>
<br/>
# Sort:<br/>
to_sort.sort()<br/>
<br/>
# Undecorate:<br/>
a_list = [item[-1for item in to_sort]<br/>

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

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

Сортировка с ключом


В Python 2.4 появился необязательный аргумент «key» в методе списка sort, который задает в свою очередь функцию от одного аргумента, используемую для вычисления ключа сравнения для каждого элемента списка. Например:
def my_key(item):<br/>
    return (item[1], item[3])<br/>
<br/>
to_sort.sort(key=my_key)<br/>

Функция my_key будет вызываться один раз для каждого элемента списка to_sort.
Вы можете собрать свою собственную ключ-функцию или использовать любую существующую функцию одного аргумента, если необходимо:
  • str.lower чтобы сортировать в алфавитном порядке независимо от регистра символов.
  • len чтобы сортировать по длине элементов (строки или контейнеры).
  • int или float чтобы сортировать по порядку номеров, как с числовыми строками вроде «2», «123», «35».


Генераторы


Мы уже видели выражения-генераторы. Мы можем разработать свои сколь угодно сложные генераторы как функции:
def my_range_generator(stop):<br/>
    value = 0 <br/>
    while value < stop:<br/>
        yield value<br/>
        value += 1<br/>
<br/>
for i in my_range_generator(10):<br/>
    do_something(i)<br/>

Ключевое слово yield превращает функцию в генератор. Когда вы вызываете генератор-функцию, вместо выполнения кода Python возвращает объект-генератор, являющийся, как мы помним, итератором; а у него есть метод next. В цикле for просто вызывается метод итератора next, пока не будет сгенерировано исключение StopIteration. Вы можете вызвать StopIteration явно или неявно, вываливающееся по окончанию работы кода, как выше.
Генераторы могут упростить обработку последовательности/итератора, поскольку нам не нужно собирать конкретно список; просто вычисляется одно значение за каждую итерацию.

Объясню, как в действительности работает цикл for. Python смотрит на последовательность, указанную после ключевого слова in. Если это простой контейнер (как список, кортеж, словарь, множество или определенный пользователем), Python преобразует его в итератор. Если этот объект уже является итератором, Python использует непосредственно его.
Затем Python многократно вызывает метод итератора next, связывает возвращаемое значение со счетчиком цикла (i в этом случае), и выполняет код тела цикла. Этот процесс повторяется снова и снова, пока не будет вызвано исключение StopIteration или выполнена инструкция break в теле цикла.
Цикл for может включать условие else (иначе), код которого будет выполнен после выхода из цикла, но не после выполнения инструкции break. Эта особенность предоставляет очень изящные решения. Условие else не всегда и не часто используется с циклом for, но может нам пригодиться. Иногда else удачно выражает логику, которая вам нужна.
Например, если вам нужно проверить условие, содержащееся в некотором элементе, любом элементе последовательности:
for item in sequence:<br/>
    if condition(item):<br/>
        break<br/>
else:<br/>
    raise Exception('Condition not satisfied.')<br/>



Пример генератора


Отфильтровать пустые строки из CSV файла (или элементы из списка):
def filter_rows(row_iterator):<br/>
    for row in row_iterator:<br/>
        if row:<br/>
            yield row<br/>
<br/>
data_file = open(path, 'rb')<br/>
irows = filter_rows(csv.reader(data_file))<br/>


Считывание строк из текстового файла


datafile = open('datafile')<br/>
for line in datafile:<br/>
    do_something(line)<br/>

Это возможно, посеольку файлы поддерживают метод next, как делают другие итераторы: списки, кортежи, словари (для их ключей),
генераторы.
Тут будьте осторожны: из-за буферизации операций с файлами, нельзя смешивать .next и .read* методы, если не используете Python 2.5+.


EAFP vs. LBYL


Проще просить прощения, чем разрешения. (It's easier to ask forgiveness than permission)
Семь раз отмерь, один отрежь. (Look before you leap)
Обычно EAFP предпочтительнее, но не всегда.
  • Утиная типизация
    Если это ходит подобно утке, крякает, как утка и выглядит, как утка, то это утка. (Гусь? Достаточно близко.)
  • Исключения
    Используйте явное указание, если объект должен быть конкретного типа. Если x должен быть строкой для того, чтобы ваш код работал, то почему бы не объявить это?

    str(x)<br/>

    и вместо попыток наобум, используйте что-то вроде:

    isinstance(x, str)<br/>



EAFP try/except Пример


Вы можете поместить код, склонный к исключительным ситуациям в блок try/except, чтобы отлавливать ошибки, и в конечном итоге, возможно получите более общее решение, чем если бы попытались предусмотреть все варианты.
try:<br/>
    return str(x)<br/>
except TypeError:<br/>
    ...<br/>

Отметьте: всегда нужно определять исключения, которые нужно перехватить. Никогда не используйте чистое условие except. Чистое except условие будет ловить все исключения, возникающие в вашем коде, чрезвычайно затрудняя отладку.

Импортирование


from module import *<br/>

Вы вероятно видели эту «дикую карту» (уайлд кард, шаблон) в выражениях импорта модулей. Возможно, она вам даже нравится. Не используйте ее.
Адаптация известного диалога:
(Внешняя Дагоба, джунгли, болото и туман.)
ЛЮК: from module import * лучше, чем явный импорт?
ЙОДА: Не лучше, нет. Быстрее, проще, соблазнительнее.
ЛЮК: Но откуда мне знать, что явный импорт лучше, чем дикая карта?
ЙОДА: Узнаешь, когда захочешь свой код через шесть месяцев прочесть попытаться.

(На всякий случай, привожу текст оригинала — примеч. перев.)
(Exterior Dagobah, jungle, swamp, and mist.)
LUKE: Is from module import * better than explicit imports?
YODA: No, not better. Quicker, easier, more seductive.
LUKE: But how will I know why explicit imports are better than
the wild-card form?
YODA: Know you will when your code you try to read six months
from now.

Импорт с дикой картой — Темная сторона Python.

Никогда!
from module import * сильно загрязняет пространство имен. Вы обнаружите объекты в вашем локальном пространстве имен, которые не ожидали получить. Вы можете увидеть имена, переопределяющие локальные, определенные ранее в модуле. Вы не сможете вычислить, откуда именно берутся эти имена. Хотя эта форма коротка и проста, ей не место в конечном коде.
Мораль: Не используйте импорт с дикой картой!
Так значительно лучше:
  • связывание имен через их модули (полное описание идентификаторов, с указанием их происхождения),
  • импорт длинных названий модулей через укороченное имя (псевдоним, алиас),
  • или явно импортируйте именно те имена, которые вам нужны.

Тревога загрязнения пространства имен!
Вместо этого,
связывайте имена через их модули (подробно описанные идентификаторы, с указанием их происхождения):
import module<br/>
module.name<br/>

или импортируйте длинные названия модулей через алиас :
import long_module_name as mod<br/>
mod.name<br/>

или явно импортируйте только те имена, которые нужны:
from module import name<br/>
name<br/>

Заметьте, что эта форма не годится для использования в интерактивном интерпретаторе, где вы можете захотеть отредактировать и перезагрузить («reload( )») модуль.

Модули и скрипты


Чтобы сделать одновременно импортируемый модуль и исполняемый сценарий:
if __name__ == '__main__':<br/>
    # script code here<br/>

Когда его импортируете, атрибут модуля __name__ установлен как имя файла без расширения ".py". Так код по условию if не будет работать, когда модуль импортируется. Когда же вы исполняете скрипт, атрибут __name__ устанавливается в значение "__main__", и код скрипта будет работать.
За исключением некоторых специальных случаев, вам не следует помещать весь код в верхнем уровне. Спрячьте код в функции, классы, методы, и закройте его с помощью if __name__ == '__main__'.


Структура модуля


"""module docstring"""<br/>
<br/>
# imports<br/>
# constants<br/>
# exception classes<br/>
# interface functions<br/>
# classes<br/>
# internal functions & classes<br/>
<br/>
def main(...):<br/>
    ...<br/>
<br/>
if __name__ == '__main__':<br/>
    status = main()<br/>
    sys.exit(status)<br/>

Вот так должен быть структурирован модуль.

Обработка командной строки


Пример: cmdline.py:
#!/usr/bin/env python<br/>
<br/>
"""<br/>
Module docstring.<br/>
"""
<br/>
<br/>
import sys<br/>
import optparse<br/>
<br/>
def process_command_line(argv):<br/>
    """<br/>
    Return a 2-tuple: (settings object, args list).<br/>
    `argv` is a list of arguments, or `None` for ``sys.argv[1:]``.<br/>
    """
<br/>
    if argv is None:<br/>
        argv = sys.argv[1:]<br/>
<br/>
    # initialize the parser object:<br/>
    parser = optparse.OptionParser(<br/>
        formatter=optparse.TitledHelpFormatter(width=78),<br/>
        add_help_option=None)<br/>
<br/>
    # define options here:<br/>
    parser.add_option(      # customized description; put --help last<br/>
        '-h''--help', action='help',<br/>
        help='Show this help message and exit.')<br/>
<br/>
    settings, args = parser.parse_args(argv)<br/>
<br/>
    # check number of arguments, verify values, etc.:<br/>
    if args:<br/>
        parser.error('program takes no command-line arguments; '<br/>
                     '"%s" ignored.' % (args,))<br/>
<br/>
    # further process settings & args if necessary<br/>
<br/>
    return settings, args<br/>
<br/>
def main(argv=None):<br/>
    settings, args = process_command_line(argv)<br/>
    # application code here, like:<br/>
    # run(settings, args)<br/>
    return 0         # success<br/>
<br/>
if __name__ == '__main__':<br/>
    status = main()<br/>
    sys.exit(status)<br/>



Пакеты


package/<br/>
    __init__.py<br/>
    module1.py<br/>
    subpackage/<br/>
        __init__.py<br/>
        module2.py<br/>

  • Используйте для организации ваших проектов.
  • Снижают затраты на поиск пути при загрузке.
  • Уменьшают конфликты импорта имен.

Пример:
import package.module1<br/>
from package.subpackage import module2<br/>
from package.subpackage.module2 import name<br/>

В Python 2.5 у нас теперь абсолютные и относительные импорты через future import:
from __future__ import absolute_import<br/>

Я не достаточно глубоко пока еще разобрался в этом, так что мы опустим эту часть нашей дискуссии.


Простое лучше сложного


Во-первых, отладка дважды труднее написания кода. Следовательно, если вы пишете код настолько умно, насколько возможно, вы, по определению, не достаточно умны, чтобы отлаживать его.
—Brian W. Kernighan, co-author of The C Programming Language and the «K» in «AWK»

Другими словами, сохраняйте простоту ваших программ!

Не изобретайте колесо


Перед написанием любого кода:
  • Посмотрите стандартную библиотеку Python.
  • Проверьте Python Package Index («Сырная лавка») http://cheeseshop.python.org/pypi
    (видимо, намек на скетч о сырной лавке, подобное нашел в Вики-учебнике — примеч. перев.)
  • Поищите в сети. Google ваш друг.


Ссылки









Статья подготовлена в Хабраредакторе, примеры кода раскрашены Habra-colorer со стилем default.
Теги:
Хабы:
Всего голосов 55: ↑48 и ↓7+41
Комментарии13

Публикации

Истории

Работа

Data Scientist
68 вакансий

Ближайшие события

2 – 18 декабря
Yandex DataLens Festival 2024
МоскваОнлайн
11 – 13 декабря
Международная конференция по AI/ML «AI Journey»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань