Итераторите са в основата на това как работи цикълът 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)
В такива сценарии, след като четецът на файлове изчерпи редовете във файла, той ще хвърли StopIteration
exception. нямаме цикъл 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
, се разпространяват правилно или по предназначение.