Dacă ai scris Python mai mult de o săptămână, le-ai folosit pe toate trei. Ai scris for x in something:. Ai scris [x*2 for x in xs]. Poate chiar ai scris yield. Și dacă cineva te-ar întreba „care e diferența dintre un iterable, un iterator, un generator și o comprehension”, probabil ai face ce face toată lumea: ai miji ochii, ai mormăi ceva despre laziness și ai schimba subiectul.
E în regulă pentru cod ocazional. Nu mai e în regulă în ziua în care trebuie să treci un CSV de 50 GB printr-un container cu memorie limitată sau să depanezi un generator care rulează misterios de două ori, fără rezultate a doua oară. Cele patru cuvinte înseamnă patru lucruri diferite. Astăzi le despărțim.
Iterable vs iterator: versiunea în două propoziții
Un iterable e orice poți pune după in într-un for. Liste, tupluri, dicționare, mulțimi, string-uri, fișiere, range-uri, clase proprii care implementează __iter__. Lucrul în sine nu ține poziția; poți face loop pe el de câte ori vrei.
Un iterator e obiectul cu stare, de unică folosință, care chiar parcurge un iterable. Ține minte unde a ajuns. Când se termină, ridică StopIteration. Odată epuizat, gata, e gata: îl repornești obținând unul nou.
xs: list[int] = [10, 20, 30] # iterable
it = iter(xs) # iterator, abia născut
print(next(it)) # 10
print(next(it)) # 20
print(next(it)) # 30
print(next(it)) # ridică StopIteration
for x in xs: e zahăr sintactic pentru „cheamă iter(xs) o dată, apoi cheamă next() pe el într-un loop până la StopIteration, apoi oprește-te”. Lista xs e iterable-ul. Obiectul returnat de iter(xs) e iterator-ul. Nu sunt același obiect, iar distincția e cea care îți permite să faci loop peste aceeași listă de două ori, fără ca ea să fie „consumată”.
O subtilitate: un iterator e și un iterable (are __iter__, care se returnează pe sine). De aceea funcționează for x in some_generator:. Dar îl poți parcurge o singură dată:
g = (x*2 for x in [1, 2, 3]) # generator expression
print(list(g)) # [2, 4, 6]
print(list(g)) # [] - deja epuizat
Dacă ai scris vreodată o funcție care returnează un generator și ai încercat să iterezi peste rezultat de două ori, asta e bug-ul.
Protocolul iterator pe clase proprii
Dacă vrei ca propria clasă să funcționeze într-un for, ai nevoie de două metode:
from typing import Iterator
class CountDown:
def __init__(self, start: int) -> None:
self.start = start
def __iter__(self) -> Iterator[int]:
# Returnează un iterator nou de fiecare dată - asta e ce face
# CountDown un *iterable*, nu un iterator.
return CountDownIterator(self.start)
class CountDownIterator:
def __init__(self, current: int) -> None:
self.current = current
def __iter__(self) -> "CountDownIterator":
return self
def __next__(self) -> int:
if self.current <= 0:
raise StopIteration
value = self.current
self.current -= 1
return value
for n in CountDown(3):
print(n)
# 3, 2, 1
E multă ceremonie pentru „numără invers”. Citindu-l, vezi de ce există yield.
Generatoare: iteratoare fără boilerplate
Un generator e un iterator construit dintr-o funcție care are yield în ea. Interpretorul face plumbing-ul __iter__/__next__/StopIteration pentru tine.
from typing import Iterator
def count_down(start: int) -> Iterator[int]:
while start > 0:
yield start
start -= 1
for n in count_down(3):
print(n)
# 3, 2, 1
Același comportament ca la clasa de mai sus. Cam o cincime din cod. Ăsta e motivul pentru care clasele iterator sunt rare în Python-ul modern: le scoți la suprafață doar când starea e cu adevărat complexă (de exemplu, o traversare de arbore care trebuie reluată în mijlocul recursiei sau un iterator care are alte metode în afară de __next__).
Când funcția ajunge la yield, se oprește. Variabilele locale, program counter-ul, stack-ul, toate înghețate. Următorul apel next() continuă din exact acel punct. Când funcția se întoarce (sau ajunge la final), generatorul ridică StopIteration automat.
yield from permite unui generator să delege altuia:
from typing import Iterator
def numbers() -> Iterator[int]:
yield from range(3) # 0, 1, 2
yield from [10, 20, 30] # 10, 20, 30
yield 99 # 99
print(list(numbers()))
# [0, 1, 2, 10, 20, 30, 99]
Fără yield from ai scrie for x in range(3): yield x: în regulă, dar mai puțin direct.
Comprehensions: zahăr sintactic pentru construit lucruri
O list comprehension construiește o listă cu un loop compact:
xs: list[int] = [1, 2, 3, 4, 5]
doubled = [x * 2 for x in xs]
# [2, 4, 6, 8, 10]
evens_doubled = [x * 2 for x in xs if x % 2 == 0]
# [4, 8]
Echivalent cu a scrie loop-ul și apelurile .append() de mână, dar mai scurt și ușor mai rapid (interpretorul îl optimizează). Aceeași sintaxă există pentru mulțimi și dicționare:
unique_lengths = {len(s) for s in ["hi", "hello", "hey"]}
# {2, 5, 3}
word_lengths = {s: len(s) for s in ["hi", "hello", "hey"]}
# {'hi': 2, 'hello': 5, 'hey': 3}
Nu există tuple comprehension. Sintaxa (x*2 for x in xs) arată ca una, dar nu e: e o generator expression. Ca să construiești un tuplu dintr-o comprehension scrii tuple(x*2 for x in xs).
Diferența de memorie: list vs generator expression
Asta e singura chestie practică ce contează cel mai mult.
# List comprehension - construiește toată lista în memorie
squares_list = [x * x for x in range(10_000_000)]
# Memorie: ~80 MB pentru o listă de 10 milioane de int. Alocată din start.
# Generator expression - nu construiește nimic, livrează la cerere
squares_gen = (x * x for x in range(10_000_000))
# Memorie: ~200 de octeți. Doar obiectul generator.
Dacă vei consuma fiecare element și vrei acces aleator mai târziu, lista e ok. Dacă le vei consuma o singură dată, în ordine, generator-ul e aproape întotdeauna alegerea corectă.
Exemplul clasic: streaming-ul unui fișier mare.
from typing import Iterator
def numeric_columns(path: str, col: int) -> Iterator[float]:
with open(path, encoding="utf-8") as f:
next(f) # sări peste header
for line in f: # fișierele sunt iteratoare de linii
parts = line.rstrip("\n").split(",")
yield float(parts[col])
total = 0.0
count = 0
for value in numeric_columns("orders_50gb.csv", col=4):
total += value
count += 1
print(total / count if count else 0.0)
Programul ăsta calculează media unei coloane dintr-un fișier de 50 GB folosind câțiva kiloocteți de RAM. Obiectul fișier e el însuși un iterator: for line in f: citește o linie la un moment dat. Funcția generator trece acele linii printr-o transformare, una câte una. Nimic nu se materializează. Asta e forma fiecărui pipeline de date cu limită de memorie pe care îl vei scrie vreodată în Python.
Același cod cu o list comprehension ar încerca să încarce toți cei 50 GB într-o listă Python. Container-ul tău ar muri OOM la 30%.
itertools: trusa standard
itertools e un modul de blocuri de construcție bazate pe generatoare. Câteva pe care le folosesc săptămânal:
import itertools
# chain: concatenează iterabile lazy
combined = itertools.chain([1, 2], [3, 4], [5])
# 1, 2, 3, 4, 5 - fără să construiască vreodată o listă combinată
# islice: feliază un iterator fără să-l convertești în listă
first_ten = list(itertools.islice(numeric_columns("huge.csv", 4), 10))
# Citește exact 10 linii din fișier, apoi se oprește.
# tee: împarte un iterator în N iteratoare independente
a, b = itertools.tee(numeric_columns("huge.csv", 4), 2)
# a și b pot fi consumate independent - dar tee buffer-izează valorile
# pe care nici unul nu le-a consumat încă. Dacă a o ia mult înaintea
# lui b, te întorci la a ține majoritatea datelor în memorie.
# groupby, accumulate, pairwise (3.10+), batched (3.12+)...
Nu enumerez tot modulul: help(itertools) e ce-ți trebuie când ai nevoie de o unealtă. Dar odată ce vezi pattern-ul (totul e un generator, totul se compune), încetezi să mai scrii loop-uri manuale pentru lucruri ca „iterează în perechi” sau „ia fiecare al n-lea element”.
Când să folosești care
Un arbore de decizie aproximativ:
- Ai nevoie de o colecție finită pe care o vei indexa sau itera de mai multe ori? Folosește o listă sau o list comprehension.
- Ai nevoie să transformi-și-iterezi-o-dată peste ceva potențial mare? Generator expression sau funcție generator.
- Ai nevoie de comportament dincolo de
__next__, să zicem un iterator cu o metodăreset()sau cu stare suplimentară? Scrie o clasă. - Compui transformări standard?
itertoolsmai întâi, cod custom doar când nimic nu se potrivește.
Greșeala de evitat: să scrii [x for x in big_thing if cond] și apoi să iterezi imediat peste el o dată. Aia e o generator expression deghizată: scoate parantezele drepte, salvează memoria.
Capcane comune
Generatoarele sunt de unică folosință. Dacă ai nevoie să parcurgi aceleași date de două ori, ori le stochezi într-o listă, ori chemi funcția generator de două ori (ceea ce îți dă un generator nou de fiecare dată).
Late binding în closures interioare comprehension-urilor. Expresia unei comprehension e evaluată lazy pentru generator expressions: variabila de loop dintr-o list comprehension e ok, dar într-o generator expression iterable-ul e legat la momentul creării, în timp ce restul e lazy. Capcana clasică:
gens = [(x * i for x in range(3)) for i in range(3)]
# Fiecare generator captează `i` prin referință. Până să iterezi,
# `i` e 2. Toate cele trei generatoare livrează aceiași multipli de 2.
Dacă vrei chiar ca fiecare generator să captureze valoarea curentă a lui i, pasează-o ca argument default sau folosește o funcție factory.
StopIteration în interiorul unui generator îl termină tăcut. De la PEP 479 (Python 3.7+), un StopIteration care scapă din interiorul unui generator e convertit în RuntimeError în loc să încheie misterios iterația. Schimbare bună, dar merită știută dacă citești cod mai vechi care se baza pe comportamentul vechi.
Asta a fost despre iterators, generators și comprehensions, nu mai sunt amestecate. Lecția următoare: decorators. Pattern-ul care învelește jumătate din codul Python third-party pe care îl imporți.
Citations (consultat 2026-05-01):
- Python Language Reference, “The for statement” - https://docs.python.org/3/reference/compound_stmts.html#the-for-statement
itertoolsmodule documentation - https://docs.python.org/3/library/itertools.html- PEP 234, “Iterators” - https://peps.python.org/pep-0234/
- PEP 255, “Simple Generators” - https://peps.python.org/pep-0255/
- PEP 380, “Syntax for Delegating to a Subgenerator” (
yield from) - https://peps.python.org/pep-0380/ - PEP 479, “Change StopIteration handling inside generators” - https://peps.python.org/pep-0479/