Pull to refresh

Comments 71

UFO just landed and posted this here
Подсчет элементов в словаре

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

А как же Counter?

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

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

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

print colors[-i - 1]

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

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


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

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


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

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

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

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

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

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

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

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



Эээм.

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

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

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

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

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

Если список colors ооочень большой, то самый быстрый способ будет сделать так:
for color in colors:
    try:
        d[color] += 1
    except KeyError:
        d[color] = 1
Готовим коллекцию
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% медленнее (на моем локальном компьютере).
А где сравнение с 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 буферизуется не по строкам).
Хм. Достаточно интересный результат. Сравнение keyerror vs defaultdict я проводил на реальных данных в одном из проектов, и там, скорей всего в силу специфики данных, keyerror оказался быстрее.
Но как видно из вашего синтетического теста, лучше по-умолчанию использовать default_dict.

А в Python 3.2 вообще нет смысла использовать для данных целей решение отличное от defaultdict :)
На картинке судя по всему Falcon атакует Python =)
В оригинальном видео еще много о чем рассказывается: о хорошем использовании контекстных менеджеров, о генераторах, о deque. Мне кажется вы несправедливо умолчали об этом.
Последнюю задачу можно решить без использования collections.defaultdict, а также короче получается:
d = {}
for name in names:
    d.setdefault(len(name), []).append(name)
Ваше решение не короче, т.к. у автора key = len(name) добавлено для улучшения читаемости и фанаты «скрипт в 3 строчки» могут ей пожертвовать. Так же ваше решение должно работать медленее.
Ещё один (анти)паттерн, который здесь несколько раз промелькнул, но не был подчёркнут:
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)} # для поиска индекса по значению
Тогда уж так:
# хорошо и даже отлично
for i,x in enumerate(xs) :
  print i, '-->', x
Чем такое:
colors = ['red', 'green', 'blue', 'yellow']
for color in reversed(colors):
    print color


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

?
Просто интересно.
Более «питонический» код, по крайней мере так говори core разработчик и я ему верю.
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], пусть даже reversed читабельней. Вот ниже дали хороший аргумент — [::-1] создает еще один список.
reversed делает со списком только одну вещь, а [start, end, step] — универсальный способ, значит, работает медленнее.
Ради интереса проверил ваши слова, получилось, что оба варианта работают с одинаковой скоростью, как 100000 прогонов по 4 элемента в списке, так и 1 прогон по 100000 элементов в списке. Во всяком случае в третьем питоне.
Слайс снимает копию со списка, а reversed возвращает итератор, проходящий по имеющемуся списку в обратном направлении.
А почему вдруг

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
Здесь дело в том что итерируя d вы не можете его изменять.
Второй же вариант, как вы правильно написали, создает список и использует его в качестве итератора, что позволяет спокойно работать с d.
Здесь дело в том что итерируя d вы не можете его изменять.


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

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
В Python 3 iteritems переименовали в items. Так что для совместимости, если скорость не критична, писать items. И второй вариант также не будет создавать список — пока вы используете Python 3.
Мое ИМХО пусть лучше будет С-style код но с четкой и понятной архитектурой, чем непонятная лапша из «красивого идиоматического Python» кода. К сожалению наблюдается обычно обратная ситуация и вся мощь и гибкость языка используется для того чтобы максимально запутать читателя такого кода.
Можно услышать обоснованную критику?
Язык С — не образец четкости и понятности. Он гораздо ближе к тому, как работает CPU, чем к тому, как работает человеческий мозг.

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

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

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


(Это не в коем случае не холивар, я на обоих языках пишу, и оба мне нравятся. Просто, если они разные, это еще не значит, что Python плохой)
Боюсь Вы не правильно поняли мой комментарий. Я сам пишу на обоих языках и люблю оба.
Я имел ввиду что питон (за счет лаконичности) способствует перегрузке функций излишней ответственностью.
Функция должна делать ровно 1 вещь и делать ее хорошо, а в питоне зачастую можно увидеть функцию длиной 40-50 строк которая тем не менее делает 100500 разных вещей которой слабо связаны между собой. Именно это я и пытался донести в своем комментарии.
Вопрос не в том как будет записан цикл по списку, а в том насколько такой код обладает четкой и понятной архитектурой и отвечает принципам SOLID.
Функция должна делать ровно 1 вещь и делать ее хорошо, а в питоне зачастую можно увидеть функцию длиной 40-50 строк которая тем не менее делает 100500 разных вещей которой слабо связаны между собой
Честно, не очень понимаю, причем тут язык. Даже не представляю, какие особенности Питона могут вынудить нарушать этот принцип.
Если дан, например, массив чисел и надо вывести только те, которые больше 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 без двух итераторов или индексов по массиву?
Там ++(array.begin()) конечно же.
Можно сделать так:
[x[1] for x in zip(array, array[1:]) if x[1] == x[0] * 2 and x[1] > 10]
Но здесь, конечно, двух итераторов не будет только внешне.
Во-первых, у вас глюк — нужно (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]

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

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

Articles