Python: колекції, частина 4/4: Все про виразах-генераторах, генераторах списків, множин і словників

Частина 1 Частина 2 Частина 3 Частина 4
imageЗаключна частина мого циклу, відвіданого роботи з колекціями. Дана стаття самостійна, може вивчатися без попереднього вивчення попередніх.

Ця стаття глибше і детальніше попередніх і тому може бути цікава не тільки новачкам, але і досить досвідченим Python-розробникам.

imageБудуть розглянуті: вираження-генератори, генератори списку, словника і множини, вкладені генератори (5 варіантів), робота з enumerate(), range().
А також: класифікація і термінологія, синтаксис, аналоги у вигляді циклів і приклади застосування.

Я постарався розглянути тонкощі і нюанси, які висвітлюються далеко не у всіх книгах і курсах, і, зокрема, відсутні у вже опублікованих на Habrahabr статтях на цю тему.

Зміст:
1. Визначення і класифікація.
2. Синтаксис.
3. Аналоги у вигляді циклу for і у вигляді функцій.
4. Виразу-генератори.
5. Генерацію стандартних колекцій.
6.Періодичність і частковий перебір.
7. Вкладені цикли та генератори.
8. Використання range().
9. Додаток 1. Додаткові приклади.
10.Додаток 2. Посилання по темі.

1. Визначення та класифікація

1.1 Що і навіщо

  • Генератори виразів призначені для компактного і зручного способу генерації колекцій елементів, а також перетворення одного типу колекцій в інший.
  • В процесі генерації або перетворення можливе застосування умов і модифікація елементів.
  • Генератори виразів є синтаксичним цукром і не вирішують завдань, які не можна було б вирішити без їх використання.

1.2 Переваги використання генераторів виразів

  • Більш короткий і зручний синтаксис, ніж генерація в звичайному циклі.
  • Більш зрозумілий і читається синтаксис ніж функціональний аналог поєднує одночасне застосування функцій map(), filter() та lambda.
  • В цілому: швидше набирати, легше читати, особливо коли подібних операцій багато в коді.

1.3 Класифікація та особливості

Відразу скажу, що існує деяка термінологічна плутанина в російських назвах того, про що ми будемо говорити.

У цій статті використовуються наступні позначення:

  • вираз-генератор (generator expression) — вираз в круглих дужках яке видає створює на кожній ітерації новий елемент за правилами.
  • генератор колекції — узагальнена назва для генератора списку (list comprehension), генератора словника (dictionary comprehension) і генератора множини (set comprehension).
image
В окремих місцях, щоб уникнути нагромадження термінів, буде використовуватися термін «генератор» без додаткових уточнень.

2. Синтаксис
Для початку наведемо ілюстрацію загального синтаксису вираження-генератора.
Важливо: цей синтаксис однаковий і для вираження-генератора і для всіх трьох типів генераторів колекцій, різниця полягає, в яких дужках він буде укладений (дивіться попередню ілюстрацію).
image

Загальні принципи важливі для розуміння:

  • Enter — це ітератор — це може бути функція-генератор, вираз-генератор, колекція — будь-який об'єкт підтримує ітерацію по ньому.
  • Умова — це фільтр при виконанні якого елемент піде в фінальне вираз, якщо елемент йому не задовольняє, він буде пропущений.
  • Фінальне вираз — перетворення кожного вибраного елемента перед його висновком або просто висновок без змін.
2.1 Базовий синтаксис
list_a = [-2, -1, 0, 1, 2, 3, 4, 5] # Нехай у нас є вихідний список
list_b = [x for x in list_a] # Створимо новий список використовуючи генератор списку
print(list_b) # [-2, -1, 0, 1, 2, 3, 4, 5]
print(list_a is list_b) # False - це різні об'єкти!

По суті, тут нічого цікавого не сталося, ми просто отримали копію списку. Робити такі копії або просто переганяти колекції типу в тип за допомогою генераторів особливого сенсу немає — це можна зробити значно простіше застосувавши відповідні методи або функції створення колекцій (розглядалися у першій статті циклу).

Потужність генераторів виразів полягає в тому, що ми можемо задавати умови для включення елемента в нову колекцію можемо робити перетворення поточного елемента з допомогою виразу або функції перед його висновком (включенням в нову колекцію).

2.2 Додаємо умова для фільтрації
Важливо: Умова перевіряється на кожній ітерації, і тільки елементи йому задовольняють йдуть в обробку вираженні.

Додамо в попередній приклад умова — брати тільки парні елементи.

# if x % 2 == 0 - залишок від ділення на 2 дорівнює нулю - число парне
list_a = [-2, -1, 0, 1, 2, 3, 4, 5] 
list_b = [x for x in list_a if x % 2 == 0]
print(list_b) # [-2, 0, 2, 4]

Ми можемо використовувати кілька умов, комбінуючи їх логічними операторами:

list_a = [-2, -1, 0, 1, 2, 3, 4, 5]
list_b = [x for x in list_a if x % 2 == 0 and x > 0]
# беремо ті х, які одночасно парні і більше нуля
print(list_b) # [2, 4]

2.3 Додаємо обробку елемента у вираженні
Ми можемо вставляти не сам поточний елемент, який пройшов фільтр, а результат обчислення виразу з ним або результат його обробки функцією.

Важливо: Вираз виконується незалежно на кожній ітерації, обробляючи кожен елемент індивідуально.

Наприклад, можемо порахувати квадрати значень кожного елемента:

list_a = [-2, -1, 0, 1, 2, 3, 4, 5]
list_b = [x**2 for x in list_a]
print(list_b) # [4, 1, 0, 1, 4, 9, 16, 25]

Або порахувати довжини рядків c допомогою функції len()
list_a = ['a', 'abc', 'abcde']
list_b = [len(x) for x in list_a]
print(list_b) # [1, 3, 5]

2.4 Розгалуження вираження
Зверніть увагу: Ми можемо використовувати (починаючи з Python 2.5) у виразі конструкцію if-else для розгалуження фінального вираження.

У такому випадку:

  • Умови розгалуження пишуться не після, а перед ітератором.
  • В даному випадку if-else це не фільтр перед виконанням вираження, а розгалуження самого виразу, тобто змінна вже пройшла фільтр, але в залежності від умов може бути оброблена по-різному!
list_a = [-2, -1, 0, 1, 2, 3, 4, 5]
list_b = [x if $ x < 0 else x**2 for x in list_a]
# Якщо x-від'ємне - беремо x, в інших випадках - беремо квадрат x
print(list_b) # [-2, -1, 0, 1, 4, 9, 16, 25]

Ніхто не забороняєкомбінувати фільтрацію і розгалуження:

list_a = [-2, -1, 0, 1, 2, 3, 4, 5]
list_b = [x**3 if $ x < 0 else x**2 for x in list_a if x % 2 == 0]
# спочатку фільтр пропускає у вираз тільки парні значення
# після цього розгалуження у виразі для негативних зводить у куб, а для решти в квадрат
print(list_b) # [-8, 0, 4, 16]

Цей же приклад у вигляді циклу
list_a = [-2, -1, 0, 1, 2, 3, 4, 5]
list_b = []
for x in list_a:
if x % 2 == 0:
if $ x < 0:
list_b.append(x ** 3)
else:
list_b.append(x ** 2)
print(list_b) # [-8, 0, 4, 16]

2.5 Покращуємо читаність
Не забуваємо, що в Python синтаксис дозволяє використовувати переноси рядків усередині дужок. Використовуючи цю можливість, можна зробити синтаксис генераторів виразів більш легким для читання:

numbers = range(10)

# Before
squared_odds = [n ** 2 for n in numbers if n % 2 == 0]

# After
squared_odds = [
n ** 2
for n in numbers
if n % 2 == 0
]

3. Аналоги у вигляді циклу for і у вигляді функцій
Як вже говорилося вище, задачі розв'язувані за допомогою генераторів виразів можна вирішити і без них. Наведемо інші підходи, які можуть бути використані для вирішення тих же завдань.

Для прикладу візьмемо просту задачу — зробимо зі списку чисел список квадратів парних чисел і вирішимо її за допомогою трьох різних підходів:

3.1 Рішення з допомогою генератора списку
numbers = range(10)
squared_odds = [n ** 2 for n in numbers if n % 2 == 0]
print(squared_odds) # [0, 4, 8, 12, 16]

3.2. Рішення з допомогою циклу for
Важливо: Кожен генератор виразів можна переписати у вигляді циклу for, але не кожен цикл for можна представити у вигляді такого виразу.

numbers = range(10)
squared_odds = []
for n in numbers:
if n % 2 == 0:
squared_odds.append(n ** 2)
print(squared_odds) # [0, 4, 8, 12, 16]

В цілому, для дуже складних і комплексних завдань, рішення у вигляді циклу може бути зрозуміліше і простіше у підтримці та доопрацювання. Для більш простих завдань, синтаксис вираження-генератора буде компактніше і легше в читанні.

3.3. Рішення з допомогою функцій.
Для початку зауважу, що вираз генератори і генератори колекцій — це теж функціональний стиль, але більш новий і кращий.

Можна застосовувати і більш старі функціональні підходи для вирішення тих же завдань, комбінуючи map(), lambda та filter().

numbers = range(10)
squared_odds = map(lambda (n): n * 2, filter(lambda (n): n % 2 == 0, numbers))
print(squared_odds) # <map object at 0x7f661e5dba20>
print(list(squared_odds)) # [0, 4, 8, 12, 16]

Незважаючи на те, що подібний приклад цілком робочий, він важко читається і використання синтаксису генераторів виразів буде більш предпочітельним і зрозумілим.

4. Вираження-генератори
Вираження-генератори (generator expressions) доступні, починаючи з Python 2.4. Основна їх відмінність від генераторів колекцій в тому, що вони видають елемент по-одному, не завантажуючи в пам'ять відразу всю колекцію.

UPD: Ще раз зверніть увагу на цей момент: якщо ми створюємо велику структуру даних без використання генератора, то вона завантажується в пам'ять цілком, відповідно, це збільшуєвитрата пам'яті Вашим додатком, а в крайніх випадках пам'яті може просто не вистачити і Ваш додаток «впаде» з MemoryError. У разі використання виразу-генератора, такого не відбувається, так як елементи створюються по одному, у момент звернення.

Приклад вираження-генератора:
list_a = [-2, -1, 0, 1, 2, 3, 4, 5]
my_gen = (i for i in list_a) # вираз-генератор
print(next(my_gen)) # -2 - отримуємо черговий елемент генератора
print(next(my_gen)) # -1 - отримуємо черговий елемент генератора

Особливості виразів-генераторів
  1. Генаратор не можна писати без дужок — це синтаксична помилка.
    # my_gen = i for i in list_a # SyntaxError: invalid syntax

  2. При передачі у функцію додаткові дужки необов'язкові
    list_a = [-2, -1, 0, 1, 2, 3, 4, 5]
    my_sum = sum(i for i in list_a)
    # my_sum = sum((i for i in list_a)) # так теж можна
    print(my_sum) # 12

  3. не Можна отримати довжину функцією len()
    # my_len = len(i for i in list_a) # TypeError: object of type 'generator' has no len()

  4. не Можна надрукувати елементи функцією print()
    print(my_gen) # <generator object <genexpr> at 0x7f162db32af0>
    

  5. Зверніть увагу, що після проходження за висловом-генератору воно залишається порожнім!
    list_a = [-2, -1, 0, 1, 2, 3, 4, 5]
    my_gen = (i for i in list_a)
    print(sum(my_gen)) # 12
    print(sum(my_gen)) # 0

  6. Вираз-генератор може бути нескінченним.
    import itertools
    inf_gen = (x for x in itertools.count()) # нескінченний генератор від 0 to нескінченності!
    Будьте обережні в роботі з такими генераторами, так як при не правильному використанні «ефект» буде як від нескінченного циклу.

  7. висловом-генератору не застосовні зрізи!
    list_a = [-2, -1, 0, 1, 2, 3, 4, 5]
    my_gen = (i for i in list_a)
    my_gen_sliced = my_gen[1:3]
    # TypeError: 'generator' object is not subscriptable

  8. З генератора легко отримувати потрібну колекцію. Це докладно розглядається в наступному розділі.
5. Генерація стандартних колекцій
5.1 Створення колекцій з виразу-генератора
Створення колекцій з виразу-генератора з допомогою функції list(), tuple(), set(), frozenset()

Примітка: Так можна створити і незмінне безліч і кортеж, так як незмінними вони стане вже після генерації.

Увага: Для рядка такий спосіб не працює! Синтаксис створення генератора словника таким чином має свої особливості, він розглянутий у наступному під-розділі.

  1. Передачею готового вираження-генератора присвоєного змінної в функцію створення колекції.

    list_a = [-2, -1, 0, 1, 2, 3, 4, 5]
    my_gen = (i for i in list_a) # вираз-генератор
    my_list = list(my_gen) 
    print(my_list) # [-2, -1, 0, 1, 2, 3, 4, 5]

  2. Написання вираження-генератора відразу усередині дужок викликається функції створення колекції.

    list_a = [-2, -1, 0, 1, 2, 3, 4, 5]
    my_list = list(i for i in list_a)
    print(my_list) # [-2, -1, 0, 1, 2, 3, 4, 5]

    Те ж саме для кортежу, множини і незмінного безлічі
    # кортеж
    my_tuple = tuple(i for i in list_a)
    print(my_tuple) # (-2, -1, 0, 1, 2, 3, 4, 5)
    
    # безліч
    my_set = set(i for i in list_a)
    print(my_set) # {0, 1, 2, 3, 4, 5, -1, -2}
    
    # незмінне безліч
    my_frozenset = frozenset(i for i in list_a)
    print(my_frozenset) # frozenset({0, 1, 2, 3, 4, 5, -1, -2})
5.2 Спеціальний синтаксис генераторів колекцій
На відміну від виразу-генератора, яке видає значення по-одному, не завантажуючи всю колекцію в пам'ять, при використанні генераторів колекцій, колекція генерується відразу цілком.

Відповідно, замість особливості виразів-генераторів перерахованих вище, така колекція буде володіти всіма стандартними властивостями характерними для колекції даного типу.

Зверніть увагу:, що для генерації множини і словника використовуються однакові дужки, різниця в тому, що у словнику вказується подвійний елемент ключ: значення.

  1. Генератор списку (list comprehension)

    list_a = [-2, -1, 0, 1, 2, 3, 4, 5]
    my_list = [i for i in list_a]
    print(my_list) # [-2, -1, 0, 1, 2, 3, 4, 5]

    Не пишіть круглі дужки в квадратних!

    list_a = [-2, -1, 0, 1, 2, 3, 4, 5]
    my_list = [(i for i in list_a)]
    print(my_list) # [<generator object <genexpr> at 0x7fb81103bf68>]

  2. Генератор безлічі (set comprehension)

    list_a = [-2, -1, 0, 1, 2, 3, 4, 5]
    my_set= {i for i in list_a}
    print(my_set) # {0, 1, 2, 3, 4, 5, -1, -2} - порядок випадковий

  3. Генератор словника (dictionary comprehension)
    перевертання словника

    dict_abc = {'a': 1, 'b': 2, 'c': 3, 'd': 3}
    dict_123 = {v: for k k, v in dict_abc.items()}
    print(dict_123) # {1: 'a', 2: 'b', 3: 'd'}
    # Зверніть увагу, ми втратили "з"! Так як значення були однакові, 
    # коли вони стали ключами, тільки останнє значення збереглося.

    Словник зі списку:
    list_a = [-2, -1, 0, 1, 2, 3, 4, 5]
    dict_a = {x: x**2 for x in list_a}
    print(dict_a) # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16, -2: 4, -1: 1, 5: 25}

    Важливо! Такий синтаксис створення словника працює лише в фігурних дужках, вираз-генератор так створити не можна, для цього використовується трохи інший синтаксис (дякую longclaps за підказку в коментарях):
    # dict_gen = (x: x**2 for x in list_a) # SyntaxError: invalid syntax
    dict_gen = ((x, x ** 2) for x in list_a) # Коректний варіант генератора-вирази для словника
    # dict_a = dict(x: x**2 for x in list_a) # SyntaxError: invalid syntax
    dict_a = dict((x, x ** 2) for x in list_a) # Коректний варіант синтаксису від @longclaps

5.3 Генерація рядків
Для створення рядка замість синтаксису-генераторів використовується метод рядка .join(), якому в якості аргументів можна передати вираз генератор.
Зверніть увагу:: елементи колекції для об'єднання в рядок повинні бути рядками!

list_a = [-2, -1, 0, 1, 2, 3, 4, 5]
# використовуємо генератор прямо в .join() одночасно приводячи елементи до строковому типу
my_str = ".join(str(x) for x in list_a)
print(my_str) # -2-1012345

6. Періодичність і частковий перебір
6.1 Робота з enumerate()
Іноді в умовах завдання в умові-фільтрі не потрібна перевірка значення поточного елемента, а перевірка на певну періодичність, тобто, наприклад, потрібно брати кожен третій елемент.

Для подібних завдань можна використовувати функцію enumerate(), задаючу лічильник при обході ітератора в циклі:

for i, x in enumerate(iterable)
тут x — поточний елемент i — його порядковий номер, починаючи з нуля

Проілюструємо роботу з індексами:

list_a = [-2, -1, 0, 1, 2, 3, 4, 5]
list_d = [(i, x) for i, x in enumerate(list_a)]
print(list_d) # [(0, -2), (1, -1), (2, 0), (3, 1), (4, 2), (5, 3), (6, 4), (7, 5)]

Тепер спробуємо вирішити реальну задачу — виберемо в генераторі списку кожен третій елемент з вихідного списку:

list_a = [-2, -1, 0, 1, 2, 3, 4, 5]
list_e = [x for i, x in enumerate(list_a, 1) if i % 3 == 0]
print(list_e) # [0, 3]

Важливі особливості роботи функції enumerate():

  1. Можливі два варіанти виклику функції enumerate():
    • enumerate(iterator) без другого параметра вважає 0.
    • enumerate(iterator, start) — починає вважати значення start. Зручно, наприклад, якщо нам треба вважати з 1, а не 0.

  2. enumerate() повертає кортеж з порядкового номера та значення поточного елемента ітератора. Кортеж у вираженні-генераторі результаті можна отримати двома способами:
    • (i, j) for i, j in enumerate(iterator) — дужки в першій парі потрібні!
    • pair for pair in enumerate(mylist) — ми працюємо відразу з парою
  3. Індекси вважаються для всіх оброблених елементів, без урахування пройшли вони в подальшому умова чи ні!

    first_ten_even = [(i, x) for i, x in enumerate(range(10)) if x % 2 == 0]
    print(first_ten_even) # [(0, 0), (2, 2), (4, 4), (6, 6), (8, 8)]

  4. Функція enumerate() не звертається до якимось внутрішнім атрибутів колекції, а просто реалізує лічильник оброблених елементів, тому нічого не заважає її використовувати для невпорядкованих колекцій не мають індексації.

  5. Якщо ми обмежуємо кількість елементів включені в результат за enumerate() лічильника (наприклад, if i < 10), то ітератор буде все одно оброблений цілком, що у разі величезної колекції буде дуже ресурс-витратно. Вирішення цієї проблеми розглядається нижче в під-розділі «Перебір частини итерируемого».
6.2 Перебір частини итерируемого.
Іноді буває завдання з дуже великої колекції або навіть нескінченного генератора отримати вибірку перших кількох елементів, що задовольняють умові.

Якщо ми використовуємо звичайне генераторний вираз з умовою обмеженням за enumerate() індексом або зріз отриманої результуючої колекції, нам у будь-якому випадку доведеться пройти всю величезну колекцію і витратити на це купу комп'ютерних ресурсів.

Виходом може бути використання функції islice() з пакету itertools.

import itertools
first_ten = (itertools.islice((x for x in range(1000000000) if x % 2 == 0), 10))
print(list(first_ten)) # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

Для тих, хто сумнівається: перевіряємо час виконання
import time
import itertools

# На генераторі з малою кількістю елементів
start_time = time.time()
first_ten = (itertools.islice((x for x in range(100) if x % 2 == 0), 10))
print(list(first_ten)) # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
elapsed_time = time.time() - start_time
print(elapsed_time) # 3.409385681152344 e-05

# На генераторі з величезною кількістю елементів
start_time = time.time()
first_ten = (itertools.islice((x for x in range(100000000) if x % 2 == 0), 10))
print(list(first_ten)) # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
elapsed_time = time.time() - start_time
print(elapsed_time) # 1.1205673217773438 e-05

# Тобто максимальна кількість елементів в генераторі range() ми збільшили на 6 порядків, 
# а час виконання залишилося того ж порядку

7. Вкладені цикли та генератори
Розглянемо більш комплексні варіанти, коли у нас цикли або самі вирази-генератори є вкладеними. Тут можливі кілька варіантів, зі своїми особливостями і сферою застосування, щоб не виникало плутанини, розглянемо їх окремо, а після наведемо загальну схему.

7.1 Вкладені цикли
У результаті генерації отримуємо одновимірну структуру.

Важливо! При працює з вкладеними циклами всередині генератора виразів порядок проходження інструкцій for in буде такою ж (зліва-направо), як і в аналогічному рішення без генератора, тільки на циклах (зверху-вниз)! Теж справедливо і при більш глибоких рівнях вкладеності.

7.1.1 Вкладені цикли for де цикли йдуть по незалежним итераторам
Загальний синтаксис: [expression for x in iter1 for y in iter2]
Застосування: генеруємо одновимірну структуру, використовуючи дані з двох ітераторів.

Наприклад, створимо словник, використовуючи кортежі координат як ключі, заповнивши для початку його значення нулями.

rows = 1, 2, 3
cols = 'a', 'b'
my_dict = {(col, row): 0 for row in rows for col in cols}
print(my_dict) # {('a', 1): 0, ('b', 2): 0, ('b', 3): 0, ('b', 1): 0, ('a', 3): 0, ('a', 2): 0}

Далі можемо ставити нові значення або отримувати їх
my_dict['b', 2] = 10 # задаємо значення координатного ключу-кортежу
print(my_dict['b', 2]) # 10 - отримуємо значення координатного ключу-кортежу

Теж можна зробити і з додатковими умовами-фільтрами в кожному циклі:

rows = 1, 2, 3, -4, -5
cols = 'a', 'b', 'abc'
# Для наочності рознесемо на декілька рядків
my_dict = {
(col, row): 0 # кожен елемент складається з ключа-кортежу і нульового знаечния
for row in rows if row > 0 # Тільки позитивні значення
for col in cols if len(col) == 1 # Тільки односимвольні
}
print(my_dict) # {('a', 1): 0, ('b', 2): 0, ('b', 3): 0, ('b', 1): 0, ('a', 3): 0, ('a', 2): 0}

Ця ж задача розв'язана з допомогою циклу
rows = 1, 2, 3, -4, -5
cols = 'a', 'b', 'abc'
my_dict = {}
for row in rows:
if row > 0:
for col in cols:
if len(col) == 1:
my_dict[col, row] = 0
print(my_dict) # {('a', 1): 0, ('b', 2): 0, ('b', 3): 0, ('b', 1): 0, ('a', 3): 0, ('a', 2): 0}

7.1.2 Вкладені цикли for де внутрішній цикл йде за результатом зовнішнього циклу
Загальний синтаксис: [expression for x in iterator for y in x].

Застосування: Стандартний підхід, коли нам треба обходити двовимірну структуру даних, перетворюючи її на «плоску» одновимірну. В даному випадку, ми в зовнішньому циклі проходимо по рядках, а по внутрішньому елементів кожного рядка нашої двовимірної структури.

Припустимо у нас є двовимірна матриця — список списків. І ми бажаємо перетворити її в плоский одновимірний список.

matrix = [[0, 1, 2, 3],
[10, 11, 12, 13],
[20, 21, 22, 23]]

# Рішення з допомогою генератора списку:
flattened = [n for row in matrix for n in row]
print(flattened) # [0, 1, 2, 3, 10, 11, 12, 13, 20, 21, 22, 23]

Таже задача, розв'язана з допомогою вкладених циклів
flattened = []
for row in matrix:
for n in row:
flattened.append(n)
print(flattened)

Витончене рішення від iMrDron
import itertools
flattened = list(itertools.chain.from_iterable(matrix))

7.2 Вкладені генератори
Вкладеними можуть бути не тільки цикли for всередині виразу-генератора, але і самі генератори.
Такий підхід застосовується коли нам треба будувати двовимірну структуру.

Важливо!: На відміну від вище прикладів з вкладеними циклами, вкладених генераторів, спочатку обробляється зовнішній генератор, потім внутрішній, тобто порядок йде справа-наліво.

Нижче розглянемо два варіанти подібного використання.

7.2.1 — Вкладений генератор всередині генератора — двовимірна з двох одновимірних
Загальний синтаксис: [[expression for y in iter2] for x in iter1]
Застосування: генеруємо двовимірну структуру, використовуючи дані з двох одновимірних ітераторів.

Для прикладу створимо матрицю з 5 стовпців і 3 рядків і заповнимо її нулями:

w, h = 5, 3 # задаємо ширину і висоту матриці
matrix = [[0 for x in range(w)] for y in range(h)]
print(matrix) # [[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]

Створення цієї матриці двома вкладеними циклами — зверніть увагу на порядок вкладення
matrix = []
for y in range(h):
new_row = []
for x in range(w):
new_row.append(0)
matrix.append(new_row)
print(matrix) # [[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]

Примітка: Після створення можемо працювати з матрицею як із звичайним двовимірним масивом
# тепер можна додавати значення за координатами (координати - індекси в списку списків)
matrix[0][0] = 1
matrix[1][3] = 3
print(matrix) # [[1, 0, 0, 0, 0], [0, 0, 0, 3, 0], [0, 0, 0, 0, 0]]

# Отримуємо значення за довільними координатами
x, y = 1, 3
print(matrix[x][y]) # 3

7.2.2 — Вкладений генератор всередині генератора — двовимірна з двовимірної
Загальний синтаксис:[[expression for y in x] for x in iterator]
Застосування: Обходимо двовимірну структуру даних, зберігаючи результат в іншу двовимірну структуру.

Візьмемо матрицю:

matrix = [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]

Зведемо кожен елемент матриці в квадрат:

squared = [[cell**2 for cell in row] for row in matrix]
print(squared) # [[1, 4, 9, 16], [25, 36, 49, 64], [81, 100, 121, 144]]

Ця ж операція у вигляді вкладених циклів
squared = []
for row in matrix:
new_row = []
for cell in row:
new_row.append(cell**2)
squared.append(new_row)
print(squared) # [[1, 4, 9, 16], [25, 36, 49, 64], [81, 100, 121, 144]]

Узагальнимо всі перераховані вище варіанти в одній схемі (повний розмір по кліку):



7.3 — Генератор итерирующийся по генератору
Так як будь-генератор може використовуватися як ітератор в циклі for, це так само можна використовувати і для створення генератора по генератору.
При цьому синтаксично це може записуватися в два вирази або об'єднуватися під вкладений генератор.

Проілюструю і таку можливість.
Припустимо у нас є два таких генератора списків:
list_a = [x for x in range(-2, 4)] # Так зроблено для подальшого приклад синтаксису, 
# звісно подібної задачі достатньо тільки range(-2, 4)
list_b = [x**2 for x in list_a]

Теж саме можна записати і в один вираз, підставивши замість list_a його генератор списку:
list_c = [x**2 for x in [x for x in range(-2, 4)]]
print(list_c) # [4, 1, 0, 1, 4, 9]

UPD від longclaps: Перевага від комбінування генераторів на прикладі складної функції f(x) = u(v(x))
list_c = [t + t ** 2 for t in (x ** 3 + x ** 4 for x in range(-2, 4))]

8. Використання range()
Говорячи про способи генерації колекцій, не можна обійти увагою просту і дуже зручну функцію range(), яка призначена для створення арифметичних послідовностей.

Особливості функції range():

  • Найбільш часто функція range() застосовується для для запуску циклу for потрібну кількість разів. Наприклад, дивіться генерацію матриці в прикладах вище.

  • В Python 3 range() повертає генератор, який при кожному до нього зверненні видає черговий елемент.

  • Исполльзуемые параметри аналогічні таким у зрізах (крім першого прикладу з одним параметром):

    • range(stop) — в даному випадку з 0 до stop-1;
    • range(start, stop) — Аналогічно прикладу вище, але можна задати початок відмінне від нуля, можна і негативне;
    • range(start, stop, step) — Додаємо параметр кроку, який може бути негативним, тоді перебір в зворотному порядку.
  • Python 2 були 2 функції:

    • range(...) яка аналогічна висловом list(range(...)) в Python 3 — тобто вона видавала не ітератор, а відразу готовий список. Тобто всі проблеми можливої браку пам'яті, описані в розділі 4 актуальні, і використовувати її в Python 2 треба дуже акуратно!
    • xrange (...), яка працювала аналогічно range(...) в Python 3 і з 3 версії була виключена.
Приклади використання:

print(list(range(5))) # [0, 1, 2, 3, 4]
print(list(range(-2, 5))) # [-2, -1, 0, 1, 2, 3, 4]
print(list(range(5, -2, -2))) # [5, 3, 1, -1]

9. Додаток 1. Додаткові приклади

9.1 Послідовний прохід по декількох списках

import itertools
l1 = [1,2,3]
l2 = [10,20,30]
result = [l*2 for l in itertools.chain(l1, l2)]
print(result) # [2, 4, 6, 20, 40, 60]

9.2 Транспозиція матриці

(Перетворення матриці, коли рядка міняються місцями зі стовпцями).

Візьмемо матрицю.

matrix = [[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]]

Зробимо її транспозицію з допомогою генератора виразів:

transposed = [[row[i] for row in matrix] for i in range(4)]
print(transposed) # [[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]

Ця ж транспозиція матриці у вигляді циклу
transposed = []
for i in range(4):
new_row = []
for row in matrix:
new_row.append(row[i])
transposed.append(new_row)
print(transposed) # [[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]

І трохи чорної магії від @longclaps
transposed = list(map(list, zip(*matrix)))
print(transposed) # [[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]

9.3 Завдання вибору робочих днів

# Формуємо список днів від 1 до 31, з яким будемо працювати
days = [d for d in range(1, 32)]

# Ділимо список днів на тижні
weeks = [days[i:i+7] for i in range(0, len(days), 7)]
print(weeks) # [[1, 2, 3, 4, 5, 6, 7], [8, 9, 10, 11, 12, 13, 14], [15, 16, 17, 18, 19, 20, 21], [22, 23, 24, 25, 26, 27, 28], [29, 30, 31]]

# Вибираємо кожного тижня тільки перші 5 робочих днів, відкидаючи інші
work_weeks = [week[0:5] for week in weeks]
print(work_weeks) # [[1, 2, 3, 4, 5], [8, 9, 10, 11, 12], [15, 16, 17, 18, 19], [22, 23, 24, 25, 26], [29, 30, 31]]

# Якщо потрібно одним списком днів - можна об'єднати
wdays = [item for sublist in work_weeks for item in sublist]
print(wdays) # [1, 2, 3, 4, 5, 8, 9, 10, 11, 12, 15, 16, 17, 18, 19, 22, 23, 24, 25, 26, 29, 30, 31]

Можна прибрати вихідні ще більш витончено, використовуючи тільки індекси
# Формуємо список днів від 1 до 31, з яким будемо працювати
days = [d for d in range(1, 32)]

wdays6 = [wd for (i, wd) in enumerate(days, 1) if i % 7 != 0] # Видаляємо кожен 7-й день
# Видаляємо кожен 6 день залишилися після першого видалення:
wdays5 = [wd for (i, wd) in enumerate(wdays6, 1) if i % 6 != 0]

print(wdays5)
# [1, 2, 3, 4, 5, 8, 9, 10, 11, 12, 15, 16, 17, 18, 19, 22, 23, 24, 25, 26, 29, 30, 31]

# Зверніть увагу, що просто об'єднати дві умови в одному if не вийде,
# як мінімум тому, що 12-й день ділиться на 6, але не випадає на останній 2 дні тижня!

# Шикарне короткий рішення від @sophist:
days = [d + 1 for d in range(31) if d % 7 < 5]

10. Додаток 2. Посилання по темі
  1. Хороша англомовна стаття з детальним поясненням що таке генератори і ітератори

    Ілюстрація з статті:image

  2. Якщо у Вас є труднощі з розумінням логіки роботи з генераторними виразами, подивіться цікаву англомовну статтю, де проводяться аналогії між генераторними виразами і роботою з SQL і таблицями Excel.

    Наприклад:
    squared_odds = [n ** 2 # SELECT
    for n in numbers # FROM
    if n % 2 == 0] # WHERE

  3. UPD від fireSparrow: Існує розширення Python — PythonQL, що дозволяє працювати з базами даних в стилі генераторів колекцій.

  4. Ілюстрована стаття англійською, досить наочно показує синтаксис генераторних виразів.

  5. Якщо потрібні додаткові приклади по темі вкладених генераторних виразів (стаття англійською).
Частина 1 Частина 2 Частина 3 Частина 4

Запрошую до обговорення:

  • Якщо я десь допустив неточність або не врахував щось важливе — пишіть в коментарях, важливі коментарі будуть пізніше додано статтю з зазначенням вашого авторства.
  • Якщо якісь моменти не зрозумілі і потрібне уточнення — пишіть ваші запитання в коментарях — або я або інші читачі дадуть відповідь, а слушні запитання з відповідями будуть пізніше додано статтю.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.