Iterator and Generator
Ітератори та генератори⚑
Генератор vs Ітератор⚑
Різниця між генераторами та ітераторами в Python полягає в тому, що ітератори використовуються для перебору групи елементів (наприклад, у списку), тоді як генератори є способом реалізації ітераторів, і використовуються для генерації значень. Генератори використовують ключове слово yield для повернення значення з функції, але, за винятком цього, вони ведуть себе як звичайні функції.
Links
- Генераторы-итераторы Python
- Итерируемый объект, итератор и генератор
- https://pavel-karateev.gitbook.io/intermediate-python/struktury-dannykh/generators
Ітерабельний об'єкт⚑
Summary
Ітерабельний об'єкт (iterable) - це об'єкт, який може повертати значення по одному за раз. Приклади: всі контейнери і послідовності (списки, рядки і т.д.), файли, а також екземпляри будь-яких класів, в яких визначений метод
__iter__()
.
Тобто, ітерабельний об'єкт - це будь-який об'єкт, від якого вбудована функція iter()
може отримати ітератор, тобто який реалізує метод __iter__
.
Ітерабельні об'єкти можна використовувати у циклі for
, а також в багатьох інших випадках, коли очікується послідовність (функції sum()
, zip()
, map()
і т.д.).
Розглянемо ітерабельний об'єкт (Iterable
). У стандартній бібліотеці він оголошений як абстрактний клас collections.abc.Iterable
:
class Iterable(metaclass=ABCMeta):
__slots__ = ()
@abstractmethod
def __iter__(self):
while False:
yield None
@classmethod
def __subclasshook__(cls, C):
if cls is Iterable:
return _check_methods(C, "__iter__")
return NotImplemented
Він має абстрактний метод __iter__
, який повинен повернути об'єкт ітератора. І метод __subclasshook__
, який перевіряє наявність у класу методу __iter__
.
class SomeIterable1(collections.abc.Iterable):
def __iter__(self):
pass
class SomeIterable2:
def __iter__(self):
pass
print(isinstance(SomeIterable1(), collections.abc.Iterable)) # True
print(isinstance(SomeIterable2(), collections.abc.Iterable)) # True
Але є один момент - це функція iter()
. Наприклад, цю функцію використовує цикл for
для отримання ітератора. Функція iter()
спочатку намагається отримати ітератор з об'єкта, викликаючи його метод __iter__
. Якщо метод не реалізовано, то вона перевіряє наявність метода __getitem__
, і якщо він реалізований, то на його основі створюється ітератор. __getitem__
повинен приймати індекс з нуля. Якщо ж жоден з цих методів не реалізовано, то виникає виключення TypeError
.
from string import ascii_letters
class SomeIterable3:
def __getitem__(self, key):
return ascii_letters[key]
for item in SomeIterable3():
print(item)
Наприклад, по list
можна ітеруватися, але сам list
ніяк не стежить, де ми зупинилися в проході по ньому. А стежить об'єкт на ім'я ListIterator
, який повертається методом iter()
і використовується, наприклад, циклом for
.
Ітератор⚑
Summary
Ітератор - об'єкт, який повертає свої елементи по одному за раз, підтримує ітерацію по послідовності. З точки зору Python він повинен мати метод
__iter__()
, який повертає сам об'єкт ітератора, і метод__next__()
, який повертає наступний елемент послідовності або викидає винятокStopIteration
, якщо більше немає елементів.
Ітератори використовуються в циклі for для ітерації по колекції. Кожен об'єкт, який підтримує ітератор, є ітерабельним, але не кожен ітерабельний об'єкт є ітератором. Наприклад рядки та словники - оскільки вони не мають методу __next__
, натомість ітерабельність забезпечується наявністю методу __getitem__
, який дозволяє отримувати доступ до елементів за їхніми індексами чи ключами.
Ітерабельність - це властивість об'єкта підтримувати ітерацію. Всі послідовності в Python є ітерабельними, оскільки вони підтримують ітерацію через свої елементи.
Ітератори представлені абстрактним класом collections.abc.Iterator
:
class Iterator(Iterable):
__slots__ = ()
@abstractmethod
def __next__(self):
"""Return the next item from the iterator. When exhausted, raise StopIteration"""
raise StopIteration
def __iter__(self):
return self
@classmethod
def __subclasshook__(cls, C):
if cls is Iterator:
return _check_methods(C, '__iter__', '__next__')
return NotImplemented
__next__
повертає наступний доступний елемент і викликає винятокStopIteration
, коли елементів не залишилося.__iter__
повертаєself
. Це дозволяє використовувати ітератор там, де очікується ітерабельний об'єкт, наприклад, в цикліfor
.__subclasshook__
перевіряє наявність у класу методів__iter__
і__next__
По ітератору можна пройтись тільки один раз.
my_list = [1, 2, 3, 4, 5]
my_iterator = iter(my_list)
print(next(my_iterator)) # 1
print(next(my_iterator)) # 2
print(next(my_iterator)) # 3
for item in my_iterator:
print(item) # 4, 5
try:
print(next(my_iterator)) # raises StopIteration
except StopIteration:
print("Iteration is done")
Генератор⚑
В залежності від контексту, може означати або функцію-генератор, або ітератор генератора (зазвичай останнє). Методи __iter__
і __next__
для генераторів створюються автоматично.
Генератор - це лінивий ітератор. Генератор не зберігає в пам'яті всі елементи, а лише внутрішній стан для обчислення наступного елемента. На кожному кроці можна обчислити лише наступний елемент, але не попередній. Пройти генератор в циклі можна лише один раз.
Переваги використання генераторів - Економія пам'яті- генератори працюють ліниво, генеруючи значення за вимогою. Можуть використовуватися для завантаження даних у міру необхідності, що корисно при роботі з великими файлами або мережевими ресурсами, що дозволяє економити пам'ять - Ефективна обробка даних - генератори полегшують обробку великих наборів даних, тому що вони можуть обробляти дані поелементно, що може бути більш ефективним, ніж завантаження та обробка всіх даних одразу.
З точки зору реалізації, генератор в Python - це мовна конструкція, яку можна реалізувати двома способами: як функцію з ключовим словом yield
або як генераторний вираз. При виклику функції або обчисленні виразу отримуємо об'єкт-генератор типу types.GeneratorType
. Класичний приклад - генератор, який породжує послідовність чисел Фібоначчі, яка, будучи нескінченною, не могла б поміститися в будь-яку колекцію. Іноді термін використовується для самої генераторної функції, а не тільки для об'єкта, який повертається.
Оскільки в об'єкті-генераторі визначені методи __next__
і __iter__
, тобто реалізований протокол ітератора, то в Python будь-який генератор є ітератором.
Коли виконання функції-генератора завершується (за допомогою ключового слова return
або досягненням кінця функції), виникає виняток StopIteration
.
Що таке генераторна функція⚑
Генераторна функція - це спеціальний тип функції, в тілі якої зустрічається ключове слово yield
. При виклику такої функції повертається об'єкт-генератор (generator object). Генератори використовуються для створення ітерабельних об'єктів, що генерують значення "на льоту", зазвичай без зберігання їх у пам'яті. Якщо звичайна функція має одну точку входу та одне значення, яку повертається, то генераторна може мати кілька точок входу і виходу.
def generate_numbers(n):
i = 0
while i < n:
yield i
i += 1
generator = generate_numbers(5) # <generator object iterate at 0x...>
for number in generator:
print(number)
Важливо пам'ятати, що генератор можна пройти тільки один раз.
Що робить yield
⚑
yield
заморожує стан функції-генератора і повертає поточне значення. Після наступного виклику __next__()
функція-генератор продовжує своє виконання з того місця, де вона була призупинена.
Що робить yield from
?⚑
В Python ключове слово yield from
спрощує делегування генераторів. Воно використовується для передачі управління іншому генератору або ітерованому об'єкту з основного генератора. Це дозволяє зменшити обсяг коду та полегшити роботу з вкладеними генераторами.
yield from
автоматично ітерується по вказаному генератору чи ітерованому об'єкту і повертає всі його значення. При цьому усі send()
, throw()
, і close()
команди передаються делегованому генератору. Якщо делегований генератор завершується з допомогою return
, значення, яке повертається, можна отримати в основному генераторі через StopIteration
.
def subgen():
yield 1
yield 2
return "Done"
def main_gen():
result = yield from subgen()
print(result)
yield 3
for val in main_gen():
print(val) # 1 2 Done 3
Для чого використовуються .close()
, .throw()
і .send()
?⚑
Методи .close()
, .throw()
і .send()
використовуються для управління генераторами в Python, розширюючи їх стандартну функціональність і дозволяючи більш складну взаємодію з ними.
.send(value)
:
Цей метод дозволяє передати значення всередину генератора. Він відновлює виконання генератора і вставляє передане значення в місце, де знаходиться виразyield
. Це корисно, якщо генератор повинен реагувати на вхідні дані під час виконання.
def counter():
total = 0
while True:
value = yield total # Yield current total and receive new value
if value is not None:
total += value
gen = counter()
print(next(gen)) # Start generator: output 0
print(gen.send(10)) # Add 10: output 10
print(gen.send(5)) # Add 5: output 15
.throw(exc_type, value=None, traceback=None)
:
Використовується для ін'єкції винятку в генератор у місце, де він знаходиться. Генератор може обробити цей виняток або дозволити його піднятися вище. Це корисно для тестування обробки помилок у генераторі.
def sample_gen():
try:
yield "Start"
except ValueError:
yield "Handled ValueError"
yield "End"
gen = sample_gen()
print(next(gen)) # Start generator
print(gen.throw(ValueError)) # Inject exception: output "Handled ValueError"
print(next(gen)) # Output "End"
.close()
:
Завершує виконання генератора, піднімаючи винятокGeneratorExit
. Якщо генератор обробляє цей виняток, він може виконати код очищення перед завершенням. Після виклику.close()
генератор не можна більше відновити.
def example_gen():
try:
yield "Running"
finally:
print("Generator is closing.")
gen = example_gen()
print(next(gen)) # Start generator
gen.close() # Close generator: triggers `finally` block
В чому відмінність [x for x in y]
від (x for x in y)
⚑
Перший вираз повертає список (списковий вираз), другий - генератор.
Як отримати список з генератора⚑
Передати його у конструктор списку: list(x for x in some_seq)
. Важливо, що після цього по генератору не можна буде ітеруватись.
Що таке підгенератор⚑
У Python 3 існують так звані підгенератори (subgenerators). Якщо у генераторній функції зустрічається пара ключових слів yield from
, за якими слідує об'єкт-генератор, то цей генератор делегує доступ до підгенератора, доки він не завершиться (не закінчаться його значення), після чого продовжує своє виконання.
Насправді, yield
є виразом. Він може приймати значення, які надсилаються у генератор. Якщо значення не надсилаються у генератор, результатом цього виразу є None
.
yield from
також є виразом. Результатом його є значення, яке підгенератор повертає у виключенні StopIteration
(для цього значення повертається за допомогою ключового слова return
).
Які методи є у генераторів⚑
__next__()
- починає або продовжує виконання функції-генератора. Результатом поточного виразуyield
будеNone
. Виконання потім продовжується до наступного виразуyield
, який передає значення до місця, де був викликаний__next__
. Якщо генератор завершується без повернення значення за допомогоюyield
, виникає виключенняStopIteration
. Зазвичай метод викликається неявно, наприклад, цикломfor
або вбудованою функцієюnext()
.send(value)
- продовжує виконання і надсилає значення у функцію-генератор. Аргументvalue
стає значенням поточного виразуyield
. Методsend()
повертає наступне значення, повернене генератором, або викликає виключенняStopIteration
, якщо генератор завершується без повернення значення. Якщоsend()
використовується для запуску генератора, єдиним допустимим значенням єNone
, оскільки ще не було виконано жодного виразуyield
, якому можна присвоїти це значення.throw(type[, value[, traceback]])
- викликає виняток типуtype
у місці, де було призупинено генератор, і повертає наступне значення генератора (або викликаєStopIteration
). Якщо генератор не обробляє даний виняток (або викликає інший виняток), то він виникає у місці виклику.close()
- викликає винятокGeneratorExit
у місці, де було призупинено генератор. Якщо генератор викликаєStopIteration
(через нормальне завершення або через те, що він вже закритий) абоGeneratorExit
(через відсутність обробки цього винятку),close
просто повертається до місця виклику. Якщо ж генератор повертає наступне значення, виникає винятокRuntimeError
. Методclose()
нічого не робить, якщо генератор вже завершений.
Чи можна отримати елемент генератора за індексом⚑
Ні, виникне помилка. Генератор не підтримує метод __getitem__
.
Що таке співпрограма⚑
Співпрограма (coroutine) - це спеціальна функція, яка може призупиняти своє виконання та передавати управління іншим корутинам, а потім продовжувати з місця, де зупинилися.
Корутини можеть мати кілька точок входу та виходу, на відміну від звичайних підпрограм, які мають одну точку входу та одну точку виходу. Також вони можуть зупиняти своє виконання будь-якої миті за допомогою спеціального оператора (наприклад, yield в Python або await в Kotlin), зберігаючи свій стан (локальні змінні та стек викликів).
Корутини працюють кооперативно - віддають управління одне одному, не конкуруючи за ресурси.
Для реалізації співпрограм використовуються розширені можливості генераторів у Python (вирази yield
і yield from
, надсилання значень у генератори).
Співпрограми корисні для реалізації асинхронних неблокуючих операцій та кооперативної багатозадачності у одному потоці без використання зворотних викликів (callback-функцій) та написання асинхронного коду у синхронному стилі.
Python 3.5 включає підтримку співпрограм на рівні мови. Для цього використовуються ключові слова async
і await
.