Генераторы словарей

Автор оригинала: Szymon Guz
  • Перевод
  • Tutorial
Некоторые замечательные возможности языка Python незаслуженно оставлены без внимания и многие программисты о них не знают. В этот раз речь пойдет о прекрасной возможности языка, делающей код яснее: генераторы словарей — однострочные выражения, возвращающие словарь. Но начнем с компактных генераторов списков и задачи удаления неуникальных элементов коллекций.

Будет интересно в основном новичкам в Python.

Генераторы списков


Самый простой способ создать список — использовать однострочное выражение — генератор списка. Он довольно часто применяется, и я встречал его во многих примерах и в коде многих библиотек.
Предположим, что у нас есть функция возвращающая какой-то список. Хороший пример — функция range(start, end), которая возвращает числа между start и end. Начиная с версии Python 3.0 она реализована как генератор и возвращает не сразу полный список, а выдает число за числом по мере необходимости. В Python 2.* для этого использовалась функция xrange(). Получение списка чисел от 1 до 10 при помощи этой функции могло бы выглядеть так:
numbers = []
for i in range(1, 11):
    numbers.append(i)

Если нам нужны только четные номера, мы могли бы реализовать это следующим образом:
numbers = []
for i in range(1, 11):
    if i % 2 == 0:
        numbers.append(i)

Генераторы списков делают код намного проще. Так выглядит выражение возвращающее список в общем виде:
[ expression for item in list if conditional ]

Используя его, первый пример можно переписать так:
numbers = [i for i in range(1, 11)]

а второе так:
numbers = [i for i in range(1, 11) if i % 2 == 0]

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

Удаление дубликатов

Другая часто встречающаяся задача при работе с коллекциями — удаление одинаковых элементов. Ее можно решить множеством методов.
Допустим мы работаем с таким списком:
numbers = [i for i in range(1,11)] + [i for i in range(1,6)]

Самый сложный способ удалить дубликаты, который мне встречался, выглядит так:
unique_numbers = []
for n in numbers:
    if n not in unique_numbers:
        unique_numbers.append(n)

Конечно и это работает, но есть решения попроще. Вы можете использовать стандартный тип множество(set). Множества не могу содержать одинаковые элементы по определению, таким образом если конвертировать список во множество — дубликаты удалятся. Но мы получим множество а не список, поэтому если мы хотим именно список уникальных значений — нужно сконвертировать еще раз:
unique_numbers = list(set(numbers))


Удаление одинаковых объектов

Совсем другая ситуация с объектами или словарями. Например у нас есть список словарей, в которых одно из значений используется в качестве идентификатора:
data = [
  {'id': 10, 'data': '...'},
  {'id': 11, 'data': '...'},
  {'id': 12, 'data': '...'},
  {'id': 10, 'data': '...'},
  {'id': 11, 'data': '...'},
]

Удаление повторов может может быть реализовано большим или меньшим количеством кода. Конечно, чем меньше — тем лучше! Длинный вариант может выглядеть, например, так:
unique_data = []
for d in data:
    data_exists = False
    for ud in unique_data:
        if ud['id'] == d['id']:
          data_exists = True
          break
    if not data_exists:
        unique_data.append(d)


Можно получить то же результат, используя возможность, о которой я узнал пару дней назад: генераторы словарей. Они имеют похожий на генераторы списков синтаксис, но возвращают словарь:
{ key:value for item in list if conditional }

Если переписать код из примера выше с использование этой фичи, останется всего одна сточка:
{ d['id']:d for d in data }.values()

В этой строчке кода создается словарь, ключами которого являются поля, которые мы приняли за уникальный идентификатор, затем с помощью метода values() получаем все значения из созданного словаря. Т.к. словарь может содержать не больше одной записи для каждого ключа — полученный в итоге список не содержит дубликатов, что нам и требовалось.
Данная возможность была добавлена в Python 3.0 и бэкпортирована в Python 2.7, В более ранних версиях для решения подобной задачи можно использовать конструкцию такого вида:
dict((key, value) for item in list if condition)

Генерируется список кортежей (пар) и передается их конструктору dict(), который берет первый элемент кортежа как ключ, а второй как значение. При таком подходе решение всё той же задачи будет выглядеть так:
dict((d['id'], d) for d in data).values()

Комментарии 27

    +1
    Эти «выражения» в Python называются генераторами, надо бы это как-то выделить, и про них уже писали
      0
      Вы меня опередили :)
        0
        Согласен, что «Однострочные выражения» — не самый изящный перевод, но лучше не придумал и не нагуглил стандартного русского названия.
        А генераторы — это немного другое.
          +1
          А генераторы — это немного другое.

          В русскоязычной литературе, что читал, это называлось генераторы-функции. А то, о чем пост — генераторы списков или выражения-генераторы.

          Что касается гугления русского названия — википедия дает ответ. Если в ней найти «List comprehension» (взял из оригинала), и перейти на русскую версию, можно познакомиться с термином «Списковое включение (Абстракция списков)». С данным термином встречаюсь впервые, полагаю, является обобщенным для всех языков.
            0
            ок, не так выразился, List comprehensions — это «генераторы списков» по-русски, Dictionary comprehensions — «генераторы словарей», соответственно.
            Они во всей русскоязычной литературе так называются, вот еще пример
              0
              Убедительно. Исправил. Спасибо)
                0
                А еще к оригинальной статье есть отличный комментарий
                But be aware that dict comprehensions works only in Python 2.7+.
                in Python 2.6 and below you can replace it with:
                dict((key, value) for item in list if condition)

                Вроде как добавили генераторы словарей в Python 3, а потом бэкпортировали во 2 ветку
                  0
                  Дельный комментарий. Добавил в статью
                0
                Кстати, мне лично нравится название genexp — generator expression.
                  0
                  generator expression — это, я так, понимаю, выражение-генератор? Ну это же не тоже самое, что генератор списка
                  0
                  List comprehensions — списковые выражения (кажется так в перводе Лутца).
                  Мне не кажется удачным использование слова «генератор», потому что генератора тут нет.
                    +1
                    Вот из Лутца прямо

                    Вы придираетесь к слову «генератор», в данном случае «генератор списка» — это то, что создает список, не более
                      0
                      Значит я ошибся с источником этого знания у меня :)

                      Вы придираетесь к слову «генератор», в данном случае «генератор списка» — это то, что создает список, не более

                      Ну, это только потому что в оригинальном термине нет слова генератор, но в языке, при этом существуют генераторы и они не тоже самое что List comprehensions.
                      Понятно, что споры о переводе/формулировках ерундовые, но всё же я не удержусь, извините.

                      Если
                      L = [x for x in range(1, 10)]
                      

                      это генератор списка.

                      То как назвать:
                      L = (x for x in range(1, 10))
                      

                      Выражение-генератор?

                      Как-то путано выходит слишком, по моему.
                        0
                        Именно выражение-генератор. И ничего путанного. Генератор списка создает список, генератор словаря создает словарь.
                        А выражение-генератор — оно не сразу создает весь список в памяти, а работает именно как генератор.

                        Вы поищите по хабру статьи про питон и генераторы, везде используются именно эти термины. Не понимаю, почему такие споры только сейчас возникли
                          0
                          Генератор списка не возвращает генератор, как и генератор словаря, зато выражение-генератор возвращает генератор.
                          Нет, это путано.

                          Ещё раз обращаю внимание на отсутствие слова генератор в оригинальном термине.
                          И как мне кажется, «списковое выражение» вполне нейтрально и подходит для первода/описания термина гораздо лучше, чем то, что предложили вы.
                            0
                            А я еще раз повторяю, это не я предложил, это принятые русскоязычные термины в Python.
                            Вот еще одна ссылка в подтверждение, я уже их достаточно привел.
                            0
                            Ну вот вам статья про питон и генераторы :)
                            Там, я, кстати, как раз использую термин «списковые выражения».

                            Ладно, спор действительно не стоящий, не будем об этом.
                0
                Новичкам было бы еще полезно написать, для какой версии Python данные примеры. В Python 3.4 xrange нет.
                  0
                  спасибо. Написал про версии и исправил на range(), с ней код универсальнее.
                  0
                  Много чего можно также слелать и через многими ненавистную reduce, например, удаление дубликатов:
                  items = [1,2,3,6,2,1,2,3,4,1,2,3,4]
                  unique = reduce(lambda stack, item: stack + [item] if not (item in stack) else stack, items, []))
                  
                    0
                    Такой вариант даже лучше предложенного, ибо сохраняет порядок элементов списка, в отличие от промежуточного множества.
                    –1
                    Вот еще, как можно сделать, но работает более медленно
                    #[i for i in range(1, 11) if i % 2 == 0]
                    list(filter((lambda x: x % 2 == 0), range(1, 11)))
                    
                    • НЛО прилетело и опубликовало эту надпись здесь
                        0
                        Послушайте, ну писать туториал до того как вы прочитали docstring с метода это же нехорошо:

                        range():

                        range(stop) -> list of integers
                        range(start, stop[, step]) -> list of integers

                        Return a list containing an arithmetic progression of integers.
                        range(i, j) returns [i, i+1, i+2, ..., j-1]; start (!) defaults to 0.
                        When step is given, it specifies the increment (or decrement).
                        For example, range(4) returns [0, 1, 2, 3]. The end point is omitted!
                        These are exactly the valid indices for a list of 4 elements.


                        [i for i in range(1, 11) if i % 2 == 0] == range(2, 12, 2)

                          0
                          и я и автор оригинала знаем, про то что в range можно задать шаг. Это же просто пример.
                          0
                          Генератор и впрямь плохое название, в Питоне 3 генераторы — это совершенно другой зверь. Жаль, что никак не найдется нормальный термин для comprehension
                            0
                            Ну раз уж зашла речь о comprehensions, то надо вспомнить и родоначальника — set comprehension
                            oddlist = [ x%10 for x in range(100) if x%2 ] # 1 3 5 7 9 1 3 5 7 9 ...
                            emptylist = []
                            
                            oddcubes = { x : x**3 for x in range(100) if x%2 }
                            emptydict = {}
                            
                            odds = { x%10 for x in range(100) if x%2 } # 1 3 5 7 9 - дубликаты будут выкинуты
                            emptyset = set() # внезапно! {} это пустой словарь, а не пустое множество
                            

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

                            Самое читаемое