Итераторите са в основата на това как работи цикълът for в Python. Ако искаме да преминем през контейнерните обекти като списък, кортеж, речник, набор, низове, файлове и т.н., те трябва да бъдат итерируеми.

Знаем, че всичко в Python е обект. Така че, когато създаваме списък, кортеж или друг тип контейнер, те също ще бъдат обекти и всеки обект от тип контейнер в Python е итерируем обект.

По принцип има две основни неща, когато става въпрос за for цикъл или итерация през обекти: iterable & iterator. Нека видим дефиницията и на двете от официалните документи на Python,

Итерируем обект е обект, който имплементира __iter__, който се очаква да върне обект итератор.

Обект iterator имплементира __next__, който се очаква да върне следващия елемент от итерируемия обект, който го е върнал, и да повдигне StopIteration изключение, когато няма повече налични елементи.

От горните обяснения можем да бъдем сигурни, че всеки тип контейнерни данни в Python има __iter__ реализация, която връща обект iterator. Върнатият iterator ще има __next__ метод, чрез който можем да получим всеки елемент от контейнера или iterable.

Нека вземем for цикъл и да разгледаме как работи.

lst = [1,2,3,4]
for i in lst:
    print(i)
dt = {1:"one", 2:"two"}
for d in dt:
    print(dt)
# for files 
with open("somefile", 'r') as some_file:
    for line in some_file:
        print(line)
# it goes on for all container types including tuple, set, strings

В горния код, когато цикълът for се инициализира, той ще извика метода __iter__ от списъка lst. Методът __iter__ на свой ред ще върне обект iterator. Цикълът for ще използва този обект iterator и ще извика __next__ на съответния итератор.

Методът __next__ ще върне всеки елемент от контейнера при всяка итерация. След като итерираме всички елементи и не останат никакви елементи, ще бъде повдигнато изключение StopIteration, което показва края на итерацията. По-долу е даден приблизителен код, еквивалентен на това, което описах по-горе.

lst = [1, 2, 3, 4]
print(dir(lst))
# [..., '**__getitem__**', '**__iter__**', 'append', 'extend', ...]
iterator = lst.__iter__()
print(dir(iterator))
#[..., '__iter__', '**__next__**', ...]
while True:
    try:
        print(iterator.__next__())
    except StopIteration:
	    break

Това е същото за всички контейнерни обекти като кортеж, речник, набор, низове, файлове и т.н.

Създаване на персонализиран итератор:

Можем да направим всеки обект на Python итерируем чрез прилагане на __iter__ метод. Нека създадем хипотетичен клас Bag, който съдържа елементи. Ако искаме да преминем през елементите само като дадем bag обекта на for, трябва да внедрим __iter__ метод, който връща iterable.

Тогава цикълът for основно ще итерира през итератора, върнат от метода __iter__.

class Bag:
    def __init__(self, items=[]):
        self.items = items
    def __iter__(self):
        return iter(self.items)
bag = Bag(items=["pen", "money"])
for item in bag:
    print(item)

По-долу е друг пример за създаване на персонализиран итератор. Code е клас, който има необработени кодови изрази в него и можем да преминем през изразите на code обекта директно в for цикъл, тъй като имаме __iter__ реализация в класа.

class Code:
    def __init__(self, code_block, lang, raw, line_separator='\\n'):
        self.statements = [stmt.strip() for stmt in code_block.split(line_separator) 
                                if stmt.strip()]
        self.lang = lang
        self.raw = raw
    def __iter__(self):
        return iter(self.statements)
code = Code("""
                a = 5
                b = 10
                print(f"Sum of {a} & {b} is : {a+b}")
                """, "Python", True)
for statement in code:
    if code.lang == "Python":
        eval(compile(statement, "<string>", "single"))
    else:
        raise NotImplementedError

В горните примери директно използвахме вградения метод iter за създаване на обект iterator и iter работи само за вградени типове. Когато трябва да създадем итератор за дефиниран от потребителя обект, трябва да внедрим __next__ метод, който ще предостави всеки елемент за итерация (както for цикъл, така и използване на next).

По-долу е пример за генератор на четни числа, който прилага както __iter__, така и __next__.

class EvenNoGenerator:
    def __init__(self, limit=0):
        self.limit = limit
        self.cur = 0
    def __iter__(self):
        return self
    def __next__(self):
        self.cur = self.cur + 2
        if self.limit > 0 and self.cur > self.limit:
            raise StopIteration
        return self.cur
for i in EvenNoGenerator(15):
    print(i)

Ами сега, има и __getitem__

Честно казано, не бях честен с всички относно това как вградените контейнери са итерируеми, за да поддържам нещата прости за разбиране. Също така е възможно контейнерните обекти да използват друг специален метод __getitem__ за итериране на елементите.

Според pep — 0324,

Двата (__iter__ и next) метода съответстват на два различни протокола:

1. Един обект може да бъде повторен с for, ако имплементира __iter__() или __getitem__().

2. Един обект може да функционира като итератор, ако имплементира next().

Подобните на контейнери обекти обикновено поддържат протокол 1. В момента се изисква итераторите да поддържат и двата протокола. Семантиката на итерацията идва само от протокол 2; протокол 1 присъства, за да накара итераторите да се държат като последователности; по-специално, така че кодът, получаващ итератор, да може да използва for-цикъл над итератора.

Можем също така да използваме __getitem__ в нашите потребителски обекти вместо __iter__ и __next__, ако нашият случай на използване е просто да итерираме обекти на контейнер/последователност.

Примерът за клас Bag, който видяхме по-рано, може да бъде пренаписан като,

class Bag:
    def __init__(self, items=[]):
        self.items = items
    def __getitem__(self, index):
        return self.items[index]
bag = Bag(items=["pen", "money"])
for item in bag:
    print(item)

StopIteration и други изключения

Python разчита на StopIteration изключение, за да определи края на итерация. Така че, ако пишете персонализиран итератор, тогава ако повдигнете StopIteration изключение във вашия __next__ метод

class Bag:
    def __init__(self, items=[]):
        self.items = items
        self.cur = 0
    def __iter__(self):
        return self
    def __next__(self):
        self.cur = self.cur + 1
        if self.cur <= len(self.items):
            return self.items[self.cur-1]
        else:
            raise StopIteration
bag = Bag(items=["pen", "money"])
for item in bag:
    print(item)

В горния пример, след като итерирахме всеки елемент, ние повдигаме StopIteration изключение, за да уведомим цикъла for, че няма повече елементи за итерация.

Трябва да внимаваме къде вдигаме StopIteration. Ако го повдигнем преждевременно, нашата итерация приключва сама и въвежда грешки в нашия код. Позволете ми да променя горния код, за да повдигна StopIteration в if вместо else,

class Bag:
    def __init__(self, items=[]):
        self.items = items
        self.cur = 0
    def __iter__(self):
        return self
    def __next__(self):
        self.cur = self.cur + 1
        if self.cur <= len(self.items):
            print("Not interested in iteration!")
            raise StopIteration
        else:
            return self.items[self.cur-1]
bag = Bag(items=["pen", "money"])
for item in bag:
    print(item)

Тогава изходът ще бъде просто,

Not interested in iteration!

Също така, този StopIteration трябва да се обработва, когато правим итерацията ръчно вместо for цикъл. Да кажем, че имаме клас FileReader, който има както __iter__, така и __next__.

class FileReader:
    def __init__(self, file, mode):
        self.file = open(file, mode)
        self.mode = mode
    def __iter__(self):
        return self
    def __next__(self):
        return next(self.file)
    def close(self):
        self.file.close()

Можем да създадем обект от класа и да преминем през всеки ред от файла с помощта на for.

for line in FileReader('testfile', 'r'):
    print(line)

Но ако имаме случай на използване, при който трябва да четем произволно всеки ред на различни места в кода вместо for, можем да използваме next, за да получим редовете.

reader = FileReader('testfile', 'r')
line = next(reader)
print(line)
# do something with the line
line = next(reader)
print(line)

В такива сценарии, след като четецът на файлове изчерпи редовете във файла, той ще хвърли StopIterationexception. нямаме цикъл for или какъвто и да е друг имплицитен механизъм за улавяне на StopIteration и това ще прекъсне приложението.

Traceback (most recent call last):
  File "/module.py", line 21, in <module>
    line = next(reader)
           ^^^^^^^^^^^^
  File "/module.py", line 10, in __next__
    return next(self.file)
           ^^^^^^^^^^^^^^^
StopIteration

Така че трябва да сме сигурни, че обработваме StopIteration изрично, когато използваме метод next на итератори. Най-често ще предаваме безшумно изключенията StopIteration и докато го правим, трябва да гарантираме, че изключенията, различни от StopIteration, се разпространяват правилно или по предназначение.