Пользователь
0,0
рейтинг
2 декабря 2013 в 22:03

Разработка → Пишем красивый идиоматический Python из песочницы


Иногда сложно найти в Сети правильные, а главное актуальные «best practices» для языка. Документация, конечно же, содержит всю необходимую информацию, но отсеять нужную вещь в абзацах подробного (на то она и документация) описания довольно сложно. Но недавно мне улыбнулся поиск Google, и я наткнулся на очень полезные «паттерны» языка Python от одного из core разработчиков — Raymond Hettinger.

Примечание: Все рекомендации даны в нескольких вариантах: сначала идут самые «плохие» варианты, а дальше предлагается лучшая альтернатива. Актуально для версии языка 2.7, отличия для версии 3.3 читайте в примечаниях к конкретному «паттерну».


Цикл по массиву из чисел

Плохо: иногда пишут так.
for i in [0, 1, 2, 3, 4, 5]:
    print i**2

Хорошо: лучший, с генератором. Но в 32 битной системе список из миллиона чисел будет занимать ~ 32 mb памяти.
for i in range(6):
    print i**2

Отлично: самый лучший вариант. В отличии от второго xrange возвращает только одно значение за раз, и не нужно лишнюю память для хранения всего массива.
for i in xrange(6):
    print i**2

Примечание: В версии Python 3.3 xrange уже в ядре и называеться просто range.

Цикл по списку

Плохо: часто бывшие С программисты пишут так.
colors = ['red', 'green', 'blue', 'yellow']
for i in range(len(colors)):
    print colors[i]

Хорошо: лучший вариант.
colors = ['red', 'green', 'blue', 'yellow']
for color in colors:
    print color

Но если нужно пройти по списку задом на перед?

Плохо: опять, прошло из C дает о себе знать:
colors = ['red', 'green', 'blue', 'yellow']

for i in range(len(colors)-1, -1, -1):
print colors[i]

Хорошо: но в Python пишут вот так:
colors = ['red', 'green', 'blue', 'yellow']
for color in reversed(colors):
    print color


Цикл по списку с индексами

Плохо тоже что и выше.
colors = ['red', 'green', 'blue', 'yellow']
for i in range(len(colors)):
    print i, '-->', colors[i]

Хорошо: более элегантный вариант:
colors = ['red', 'green', 'blue', 'yellow']
for i, color in enumerate(colors):
    print i, '-->', color


Цикл по двум спискам

Плохо тоже что и выше.
names = ['raymond', 'rachel', 'matthew']
colors = ['red', 'green', 'blue', 'yellow']
n = min(len(names), len(colors))
for i in range(n):
    print names[i], '-->', colors[i]

Хорошо: с двух списков делаем один список кортежей. Проблема в том что zip использует больше памяти чем первый вариант.
names = ['raymond', 'rachel', 'matthew']
colors = ['red', 'green', 'blue', 'yellow']
for name, color in zip(names, colors):
    print name, '-->', color

Отлично: в отличии от zip, izip использует кэширование, что помогает существенно сэкономить память.
names = ['raymond', 'rachel', 'matthew']
colors = ['red', 'green', 'blue', 'yellow']
for name, color in izip(names, colors):
    print name, '-->', color

Примечание: В версии Python 3.3 izip вписан в ядро и называется просто zip.

Сортировка списка по алгоритму

Плохо: используя функцию для сравнения.
colors = ['red', 'green', 'blue', 'yellow']

def compare_length(c1, c2):
    if len(c1) < len(c2): return -1
    if len(c1) > len(c2): return 1
    return 0

print sorted(colors, cmp=compare_length)


Хорошо: используя сортировку по ключу. Использует намного меньше памяти.
colors = ['red', 'green', 'blue', 'yellow']

print sorted(colors, key=len)

Примечание: Метод cmp убран с ядра Python 3.x.

Цикл по ключам словаря

Обычный способ возвращает ключи. При таком цикле происходит итерация словаря, поэтому в процессе его изменять нельзя.
for k in d:
    print k

Для изменения словаря в цикле используйте цикл по ключам (Пример: удаление всех ключей начинающихся с R):
for k in d.keys():
    if k.startswith('R'):
        del d[k]

В этом случае d.keys() делает копию ключей словаря, что позволяет нам свободно работать с оригинальной структурой.

Цикл по ключам и значением словаря

Плохо: цикл по ключам и возвращение значение по последним. Медленный способ:
for k in d:
    print k, '-->', d[k]

Хорошо: быстрее делать цикл по значениях:
for k, v in d.items():
    print k, '-->', v

Отлично: Но самый лучший и быстрый способ это использовать итератор:
for k, v in d.iteritems():
    print k, '-->', v


Соединение двух списков в один словарь

Очень быстрый метод, используется только один кортеж для генерации словаря.
names = ['raymond', 'rachel', 'matthew']
colors = ['red', 'green', 'blue']

d = dict(izip(names, colors))
# d будет иметь следующее значение: 
# {'matthew': 'blue', 'rachel': 'green', 'raymond': 'red'}


Подсчет элементов в словаре

Плохо: обычный способ:
colors = ['red', 'green', 'red', 'blue', 'green', 'red']
d = {}

for color in colors:
    if color not in d:
        d[color] = 0
    d[color] += 1

#{'blue': 1, 'green': 2, 'red': 3}

Хорошо: использует функцию get():
colors = ['red', 'green', 'red', 'blue', 'green', 'red']
d = {}

for color in colors:
    d[color] = d.get(color, 0) + 1

Отлично: самый продвинутый способ это использовать defaultdict(). Но вы должны знать как он работает.
d = defaultdict(int)
for color in colors:
    d[color] += 1


Группирование элементов списка

Плохо: если нужно сгруппировать элементы списка по некоторому признаку (в примере — длина строки) часто используют такой метод:
names = ['raymond', 'rachel', 'matthew', 'roger', 'betty', 'melissa', 'judith', 'charlie']
d = {}

for name in names:
    key = len(name)
    if key not in d:
        d[key] = []
    d[key].append(name)
{5: ['roger', 'betty'], 6: ['rachel', 'judith'], 7: ['raymond', 'matthew', 'melissa', 'charlie']}

Хорошо: но есть способ гораздо элегантней и быстрее:
d = defaultdict(list)
for name in names:
    key = len(name)
    d[key].append(name)


Итог

На сегодня все. Надеюсь эти тривиальные, но полезные примеры помогут кому-то улучшить свой код, как они помогли это сделать мне. Их автором является Raymond Hettinger (@raymondh), Python Core Developer.

UPD: Видео доклада на PyCon 2013, с которого были взяты примеры кода: http://youtu.be/OSGv2VnC0go (на английском), содержит еще очень много интересной информации не перенесенной в статью, а также юмор и харизму докладчика.
Иван @Aft3rmath
карма
9,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (71)

  • +18
    Всё-таки, что бы не говорили, а питон — красивый язык. Не настолько, конечно, как хаскель, но на получение эстетического удовольствия от лицезрения кода целых программ (а не отдельных кусков, как в большинстве других языков) хватает, а это, на мой взгляд, довольно важное свойство языка.
  • +6
    Подсчет элементов в словаре

    Отлично: само продвинутый способ это использовать defaultdict().

    А как же Counter?

    У izip и подобных (а также, у zip из 3 питона) есть очень неприятная особенность: по созданным таким образом объектам нельзя два раза пройтись, нужно заново создавать. Иногда хотелось бы, чтобы в таких вещах было как в хаскеле :)
    • +1
      Можно «скопировать» итератор с помощью tee.
      • +1
        Можно, и заодно потерять в скорости по сравнению с обычным zip. Причём в 3 питоне обычный встроенный zip делает итератор, а не список, поэтому в случае возможной необходимости многократной итерации нужно не забывать писать list(zip(...)). Если этого не сделать, то никакого исключения не будет, просто второй раз будет 0 итераций.
    • +1
      Здесь где‐то было сравнение скорости, Counter сильно отстаёт. Если я не ошибаюсь, Counter написан на Python, defaultdict — на C.
      • +1
        Но всё-таки Counter явно самый идиоматический :)
  • +3
    Немного не понял, чем отличается проход по списку задом наперед (в стиле С) от прямого прохода?

    Возможно, имелось в виду:
    print colors[-i]
    

    или даже более привычный для сишника синтаксис:
    print colors[len(colors)-i]
    
    • +1
      Возможно, имелось в виду:
      print colors[-i]

      print colors[-i - 1]

      или даже более привычный для сишника синтаксис:
      print colors[len(colors)-i]

      Плохая привычка — вызывать функцию с одним и тем же аргументом на каждой итераци :)
      • +2
        Плохая привычка


        Согласен. Благо, этот пример изначально в категории «плохой», поэтому можно не оправдываться :)
    • 0
      Дико извиняюсь, имелось в виду:

      for i in range(len(colors)-1, -1, -1):
          print colors[i]
      


      Исправил.
  • +2
    Спасибо за статью :) Все это уже знал, но все ровно приятно перечитывать подобные вещи снова и снова :)
  • +4
    > в отличии от zip, izip использует кэширование, что помогает существенно сэкономить память.

    И что именно кэширует izip?
    • 0
      просто, наверно, тоже итераторная версия.
      • +9
        Он действительно возвращает итератор и ничего не кеширует.
  • +1
    Стоит упомянуть, что iteritems тоже в Python 3 убрали.

    Если это перевод, можно ссылку на оригинал?

    Ваше объяснение разницы между range и xrange, zip и izip какое-то кривое.
    • 0
      Оригинала нету, все было взято с доклада на PyCon 2013.
      Ссылку на видеозапись добавил в конце поста.
  • +1
    Наверное, стоит добавить, что кеширующая über-функция izip лежит в модуле itertools.

    Сам модуль itertools, кстати — сокровищница.
  • 0
    Группирование элементов списка

    А еще лучше помнить про библиотечные функции c:

    itertools.groupby(names, len) 
    
    • 0
      Вы сами-то пробовали это запускать? Так группируются только подряд идущие элементы с одинаковым ключом, что видно даже на приведённом в статье списке names.
  • 0
    О, спасибо большое! Всегда использовал range(len(list)), если нужно было пройтись не по элементам, а по индексам. Буду отучать себя потихоньку =)
  • 0
    Посдчёт элементов в стловаре
    Отлично: само продвинутый способ это использовать defaultdict(). Но вы должны знать как он работает.

    d = defaultdict(int)
    for color in colors:
        d[color] += 1
    



    Эээм.

    >>> d = {'test': 123, 'one':321}
    >>> d
    {'test': 123, 'one': 321}
    >>> len(d)
    2
    

    • 0
      В оригинально примере идёт подсчёт одинаковых элементов с хранением результатов в словаре (то есть, {'red': 3, 'green': 2, 'blue': 1}), а в Вашем примере просто длина словаря. Это ведь не одно и то же.
      • +2
        А, тьху. Только сейчас понял, о чём речь. Прошу прощения тогда, да. Заглавие просто показалось слегка двусмысленным:)
        • 0
          Извините, первые статьи они такие… :)
  • +2
    Вы бы вычитали статью, ошибок куча.

    «метод cmp убран с ядра Python 3.x», «длинна строки», «делаем с двух списков один список кортежей» и так далее.
    • 0
      В личку, пожалуйста.
      Перечитаю еще раз.
      • +1
        Если бы тут просто было навыделять ошибок и отправить в личку — я так и делал, а искать, собирать куда-то, потом писать в виде списка в личку — увольте. Я заколебался этим заниматься, если честно.
        • +3
          Очень скоро в резюме в графе «владение языками» можно будет писать русский язык.
        • +2
          Статью надо бы начинать:

          Плохо: иногда пишут так.
          называеться

          Хорошо:
          называется
          • +1
            Статью нужно было начинать с приличного названия.
            Например, «Пишем красивый идиоматичный код на Python». Иначе — эквивалентно «Пишем красивый идиоматический русский» %)
            С применимостью слова «идиоматический», кстати, я бы тоже поспорил.
  • +2
    Подсчет элементов в словаре

    Если список colors ооочень большой, то самый быстрый способ будет сделать так:
    for color in colors:
        try:
            d[color] += 1
        except KeyError:
            d[color] = 1
    
    • 0
      можно короче
      for color in colors:
          d.setdefault(color, 0)
          d[color] += 1
      

      • 0
        Готовим коллекцию
        import random
        a = [random.randint(1, 10000) for a in range(10000000)]
        


        Запускаем код A:
        d = {}
        for i in a:
            try:
                d[i] += 1
            except KeyError:
                d[i] = 1
        


        Запускаем код B:
        d = {}
        for i in a:
           d[i].setdefault(i, 0)
           d[i] += 1
        


        Ваш код на ~30% короче и на ~20% медленнее (на моем локальном компьютере).
        • +3
          А где сравнение с defaultdict?
          from timeit import timeit
          import random
          from collections import defaultdict
          
          a = [random.randint(1, 10000) for a in range(10000000)]
          
          
          def dictionary_setdefault(l, d={}):
              for i in l:
                  d.setdefault(i, 0)
                  d[i] += 1
          
          
          def dictionary_keyerror(l, d={}):
              for i in l:
                  try:
                      d[i] += 1
                  except KeyError:
                      d[i] = 1
          
          
          def defaultdict_simple(l, d=defaultdict(int)):
              for i in l:
                  d[i] += 1
          
          
          for func in (dictionary_setdefault, dictionary_keyerror, defaultdict_simple):
              import sys
              sys.stderr.write('Processing {0}\n'.format(func))
              print (timeit('import __main__; __main__.{0}(__main__.a)'.format(func.__name__), number=1))
          

          У меня defaultdict самый быстрый:
          Processing <function dictionary_setdefault at 0x7f16a635c758>
          4.41468501091
          Processing <function dictionary_keyerror at 0x7f16a635ced8>
          2.28980493546
          Processing <function defaultdict_simple at 0x7f16a635cf50>
          2.04581904411
          (Python 2.7.5). Python 3.2.5:
          4.152358055114746
          2.3733527660369873
          1.9591529369354248
          Processing <function dictionary_setdefault at 0x7ff5409bda68>
          Processing <function dictionary_keyerror at 0x7ff5409bd9e0>
          Processing <function defaultdict_simple at 0x7ff5409bd518>
          (по всей видимости, stderr буферизуется не по строкам).
          • 0
            Хм. Достаточно интересный результат. Сравнение keyerror vs defaultdict я проводил на реальных данных в одном из проектов, и там, скорей всего в силу специфики данных, keyerror оказался быстрее.
            Но как видно из вашего синтетического теста, лучше по-умолчанию использовать default_dict.

            А в Python 3.2 вообще нет смысла использовать для данных целей решение отличное от defaultdict :)
  • –2
    На картинке судя по всему Falcon атакует Python =)
  • +1
    В оригинальном видео еще много о чем рассказывается: о хорошем использовании контекстных менеджеров, о генераторах, о deque. Мне кажется вы несправедливо умолчали об этом.
  • 0
    Последнюю задачу можно решить без использования collections.defaultdict, а также короче получается:
    d = {}
    for name in names:
        d.setdefault(len(name), []).append(name)
    
    • +1
      Ваше решение не короче, т.к. у автора key = len(name) добавлено для улучшения читаемости и фанаты «скрипт в 3 строчки» могут ей пожертвовать. Так же ваше решение должно работать медленее.
  • –1
    Ещё один (анти)паттерн, который здесь несколько раз промелькнул, но не был подчёркнут:
    xs = ['zero', 'one', 'two', 'three', 'four']
    # плохо
    for i in xrange(len(xs)) :
      print i, '-->', xs[i]
    # ужасно
    i = 0
    for x in xs :
      print i, '-->', xs[i]
      i += 1
    # хорошо и даже отлично
    for i,x in enumerate(xs) :
      print i, '-->', xs[i]
    

    Т.е. когда нужны не только сами элементы, но и их индексы, стоит использовать генератор кортежей (индекс, элемент).
    Причём эта штука работает и в генераторных выражениях (list/set/dict/generator comprehension):
    show = ['xs[%d]=%s' for i,x in enumerate(xs) if i%2==0] # xs[0]=zero, xs[2]=two, xs[4]=four
    look = {x:i for i,x in enumerate(xs)} # для поиска индекса по значению
    
    • 0
      Тогда уж так:
      # хорошо и даже отлично
      for i,x in enumerate(xs) :
        print i, '-->', x
      
  • +1
    Чем такое:
    colors = ['red', 'green', 'blue', 'yellow']
    for color in reversed(colors):
        print color
    


    Лучше такого:
    colors = ['red', 'green', 'blue', 'yellow']
    for color in colors[::-1]:
        print color
    

    ?
    Просто интересно.
    • +2
      Более «питонический» код, по крайней мере так говори core разработчик и я ему верю.
    • 0
      www.python.org/dev/peps/pep-0020/

      The Zen of Python. Первые строки
      Beautiful is better than ugly.
      Explicit is better than implicit.
      Simple is better than complex.
      Complex is better than complicated..
      • –1
        Окей, тогда другой вопрос — что является превалирующим: субъективное понятие красоты или более-менее объективная простота чтения? :)
        • +2
          Моё личное мнение — в Питоне это одно и тоже. Красота и простота — вот за что мне нравится этот язык.
          • 0
            Тогда происходит конфликт: мне больше нравится [::-1], пусть даже reversed читабельней. Вот ниже дали хороший аргумент — [::-1] создает еще один список.
    • 0
      reversed делает со списком только одну вещь, а [start, end, step] — универсальный способ, значит, работает медленнее.
      • 0
        Ради интереса проверил ваши слова, получилось, что оба варианта работают с одинаковой скоростью, как 100000 прогонов по 4 элемента в списке, так и 1 прогон по 100000 элементов в списке. Во всяком случае в третьем питоне.
        • 0
          Мне кажется, это такой умный интерпретатор.
          P.S. Вот всё и объяснилось: habrahabr.ru/post/204476/#comment_7055038
    • +1
      Слайс снимает копию со списка, а reversed возвращает итератор, проходящий по имеющемуся списку в обратном направлении.
      • 0
        Ага, благодарю.
  • +1
    А почему вдруг

    for k in d:
        k, d[k]
    

    медленнее, чем

    for k, v in d.items():
        k, v
    


    Мало того, что первый вариант быстрее, так он еще и не создает в памяти список для итерации:
    %timeit for k in d: k, d[k]
    1000 loops, best of 3: 1.33 ms per loop
    
    %timeit for k, v in d.items(): k, d
    1000 loops, best of 3: 1.92 ms per loop
    
    • 0
      Здесь дело в том что итерируя d вы не можете его изменять.
      Второй же вариант, как вы правильно написали, создает список и использует его в качестве итератора, что позволяет спокойно работать с d.
      • 0
        Здесь дело в том что итерируя d вы не можете его изменять.


        Если дело в этом, то тогда чем хорош третий вариант? Он точно так же, как и первый вариант, не даст изменить размер словаря во время итерации :)
    • 0
      Быстрее и не создает в памяти список:

      d = {i:i for i in xrange(40 * 1000 * 1000)}

      timeit for k in d: k, d[k]
      1 loops, best of 3: 2.63 s per loop

      timeit for k, v in d.iteritems(): k, d
      1 loops, best of 3: 2.38 s per loop
    • 0
      В Python 3 iteritems переименовали в items. Так что для совместимости, если скорость не критична, писать items. И второй вариант также не будет создавать список — пока вы используете Python 3.
  • –5
    Мое ИМХО пусть лучше будет С-style код но с четкой и понятной архитектурой, чем непонятная лапша из «красивого идиоматического Python» кода. К сожалению наблюдается обычно обратная ситуация и вся мощь и гибкость языка используется для того чтобы максимально запутать читателя такого кода.
    • +1
      Можно услышать обоснованную критику?
      • 0
        Язык С — не образец четкости и понятности. Он гораздо ближе к тому, как работает CPU, чем к тому, как работает человеческий мозг.

        Если «идиоматические» конструкции в примере кажутся «непонятной лапшой», так это потому что большинство программистов училось писать на С-подобных языках и привыкло писать на С-подобных языках.

        Собственно, в большинстве примеров и описываются конструкции, которые пишут люди, привыкшие к С, но очень поверхностно освоившие Python. Человек, который сразу изучал Python, возможно, даже не сразу поймет, что хотели сказать конструкцией

        for i in range(len(colors)):
            print colors[i]
        


        (Это не в коем случае не холивар, я на обоих языках пишу, и оба мне нравятся. Просто, если они разные, это еще не значит, что Python плохой)
        • 0
          Боюсь Вы не правильно поняли мой комментарий. Я сам пишу на обоих языках и люблю оба.
          Я имел ввиду что питон (за счет лаконичности) способствует перегрузке функций излишней ответственностью.
          Функция должна делать ровно 1 вещь и делать ее хорошо, а в питоне зачастую можно увидеть функцию длиной 40-50 строк которая тем не менее делает 100500 разных вещей которой слабо связаны между собой. Именно это я и пытался донести в своем комментарии.
          Вопрос не в том как будет записан цикл по списку, а в том насколько такой код обладает четкой и понятной архитектурой и отвечает принципам SOLID.
          • +1
            Функция должна делать ровно 1 вещь и делать ее хорошо, а в питоне зачастую можно увидеть функцию длиной 40-50 строк которая тем не менее делает 100500 разных вещей которой слабо связаны между собой
            Честно, не очень понимаю, причем тут язык. Даже не представляю, какие особенности Питона могут вынудить нарушать этот принцип.
  • 0
    Если дан, например, массив чисел и надо вывести только те, которые больше 10, а предидущий элемент меньше в два раза. На C++ это будет как-то так

    for (auto j = array.begin(),  J=array.end(); j!=J; ++j)
        if (*j>10)
            if (*j == *(j--)*2)
                std::cout<<*j<<'\n';
    


    Можно ли это сделать на на Python без двух итераторов или индексов по массиву?
    • 0
      Там ++(array.begin()) конечно же.
    • +1
      Можно сделать так:
      [x[1] for x in zip(array, array[1:]) if x[1] == x[0] * 2 and x[1] > 10]
      
      Но здесь, конечно, двух итераторов не будет только внешне.
    • +3
      Во-первых, у вас глюк — нужно (j-1), а с j-- вы так и будете ходить по этому элементу бесконечно туда-сюда. Плюс вылет за границу, если первый элемент больше десяти. Плюс, кажется, undefined behavior из-за неопределенности порядка выполнения j-- и *j.

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

      [x for x_prev, x in zip(xs, xs[1:]) if x > 10 and x == x_prev * 2]
      


      Или быстрее с islice, который не копирует, а создает ленивый итератор со сдвигом:

      [x for x_prev, x in zip(xs, islice(xs, 1, None)) if x > 10 and x == x_prev * 2]
      

      • 0
        j-- создаёт новый объект, так что всё нормально undefined нет. На счёт вылета я там поправил уже.

        С islice хорошо да. Но опять же у вас два итератора всегда а не только когда нужно.
        • 0
          j--, по определению, изменяет существующий объект. Она возвращает новый объект, да — копию j до изменения. Кстати, опять же, из-за этого вы на самом деле сравниваете элемент с самим собой, а не с предыдущим (если забыть про UB с неупорядоченностью выполнения).
          • 0
            да, вы правы, нужно *(j-1)
            • +1
              В-общем, подводя итог: на Питоне без двух итераторов здесь не обойтись, и работать будет медленнее, но зато сразу правильно ;)
              • 0
                Вы просто лучше программист чем я, я на питонее вообще не сообразил как это написать :)

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.