Python'da üreteçler (generator): yield, yineleyiciler ve liste, sözlük üretimleri
Python'da üreteç (generator), değerleri tek tek ve istendikçe üreten bir nesnedir; tüm diziyi bellekte tutmaz. Yield anahtar sözcüğüyle yazılan bir fonksiyonla ya da parantez içindeki bir üreteç ifadesiyle oluşturulur. Liste, sözlük ve küme üretimleri (comprehension) hazır bir koleksiyonu tek satırda kurar: [x * 2 for x in xs], {k: v for k, v in pairs}, {x for x in xs}.
Python'da üreteç (generator), değerleri birer birer ve yalnızca istendiğinde üreten bir nesnedir; tüm diziyi bellekte tutmaz. Üreteçleri iki şekilde oluşturursunuz: yield içeren bir fonksiyonla ya da parantez içinde bir üreteç ifadesiyle. Bunların yanında liste, sözlük ve küme üretimleri (comprehension) de bulunur; bunlar hazır bir koleksiyonu tek satırda kurmanın kısa yoludur. Bu yazıda python yield'ın nasıl çalıştığını, üreteç ile yineleyici (iterator) arasındaki farkı ve comprehension'ları okunaklı tutmanın yolunu inceliyoruz.
Yineleyiciler: for döngüsünün temeli
Üreteçleri anlamak için önce yineleyicileri bilmek gerekir. Yinelenebilir nesne (iterable), üzerinde for ile dolaşılabilen şeydir: liste, metin, sözlük, dosya. Yineleyici (iterator) ise mevcut konumunu hatırlayan ve her next() çağrısında sıradaki elemanı veren nesnedir; elemanlar bittiğinde StopIteration fırlatır.
nums = [10, 20, 30]
it = iter(nums) # listeden bir yineleyici al
print(next(it)) # 10
print(next(it)) # 20
print(next(it)) # 30
next(it) # StopIteration
for döngüsü tam olarak bunu yapar: iter()'i çağırır, ardından StopIteration gelene kadar next()'i. Kendi yineleyicinizi __iter__ ve __next__ metotlarına sahip bir sınıfla yazabilirsiniz:
class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current <= 0:
raise StopIteration
value = self.current
self.current -= 1
return value
print(list(Countdown(3))) # [3, 2, 1]
Bu çalışır, ama çok fazla tekrarlayan kod gerektirir. Üreteçler aynı işi üç satırda yapar.
Python'da üreteç nedir ve yield nasıl çalışır
Üreteç fonksiyonu, gövdesinde yield bulunan sıradan bir fonksiyondur. Çağrıldığında kodu çalıştırmaz, bir üreteç nesnesi döndürür. Her next() çağrısı gövdeyi bir sonraki yield'e kadar çalıştırır, değeri verir ve fonksiyonu yerel değişkenleriyle birlikte dondurur. Bir sonraki next() tam kaldığı yerden devam eder.
def countdown(start):
while start > 0:
yield start
start -= 1
gen = countdown(3)
print(gen) # <generator object countdown at 0x...>
print(next(gen)) # 3
print(list(gen)) # [2, 1] — durulan yerden devam etti
Fonksiyon sonuna geldiğinde (ya da return ile çıktığında) üreteç kendiliğinden StopIteration fırlatır ve for döngüsü düzgünce biter. Yani yield ile return arasındaki fark şudur: return fonksiyonu bitirir ve tek bir değer döndürür; yield fonksiyonu duraklatır ve istediğiniz kadar değer verebilir.
Üreteç ile yineleyici arasındaki farklar
Her üreteç bir yineleyicidir: __iter__ ve __next__ metotlarına sahiptir. Ama her yineleyici üreteç değildir. Fark, oluşturma biçiminde ve sunduğu imkânlarda.
| Yineleyici (sınıf) | Üreteç | |
|---|---|---|
| Nasıl oluşturulur | __iter__ ve __next__ olan sınıf | yield içeren fonksiyon ya da (x for x in ...) |
| Durum (state) | elle, öznitelikler içinde tutulur | yerel değişkenlerde otomatik saklanır |
| StopIteration | sizin fırlatmanız gerekir | fonksiyon bitince kendisi fırlatır |
| Ek özellikler | yazdıklarınız kadar | send(), throw(), close() |
İkisinin ortak önemli özelliği: yalnızca bir kez dolaşılabilirler. Tükenmiş bir üreteç üzerinde tekrar for döngüsü çalıştırırsanız hiçbir şey gelmez; yeni bir üreteç oluşturmanız gerekir.
Sonsuz üreteç ve büyük dosyalar
Üreteç bitmek zorunda değildir. Sonsuz bir dizi, yalnızca sınırlı sayıda eleman alıyorsanız güvenlidir:
from itertools import islice
def ids():
n = 1
while True:
yield f"ID-{n:04d}"
n += 1
print(list(islice(ids(), 3))) # ['ID-0001', 'ID-0002', 'ID-0003']
En pratik kullanım, büyük bir dosyayı satır satır işlemektir. Üreteçler zincirlenerek bir boru hattı (pipeline) kurulabilir; bellekte her an yalnızca bir satır bulunur, tüm dosya değil:
def read_lines(path):
with open(path, encoding="utf-8") as f:
for line in f:
yield line.rstrip("\n")
def only_errors(lines):
for line in lines:
if "ERROR" in line:
yield line
for line in only_errors(read_lines("app.log")):
print(line)
yield from ve send
yield from, başka bir yinelenebilir nesnenin tüm değerlerini aktarır; iç içe döngüye gerek kalmaz:
def flatten(groups):
for g in groups:
yield from g
print(list(flatten([[1, 2], [3], [4, 5]]))) # [1, 2, 3, 4, 5]
send() metodu üreteç içine bir değer gönderir; bu değer yield ifadesinin sonucu olur. İlk send'den önce üreteci next() ile başlatmak gerekir. Örneğin hareketli ortalama hesaplayan bir üreteç böyle yazılır:
def averager():
total, count = 0, 0
avg = None
while True:
value = yield avg
total += value
count += 1
avg = total / count
a = averager()
next(a) # ilk yield'e kadar ilerle
print(a.send(10)) # 10.0
print(a.send(20)) # 15.0
Uygulama kodunda send nadiren kullanılır, ama mülakatlarda sık sorulur.
range bir üreteç midir?
Hayır. range tembel (lazy) bir dizidir: o da tüm sayıları tutmaz, ama indeksleme, len(), in kontrolü destekler ve istediğiniz kadar dolaşılabilir. Üreteç bunların hiçbirini yapamaz.
r = range(5)
print(r[2], len(r), 3 in r) # 2 5 True
print(list(r), list(r)) # ikisinde de [0, 1, 2, 3, 4]
Liste üretimi: list comprehension
Liste üretimi (list comprehension), [ifade for x in koleksiyon] biçimindedir. Yeni bir liste oluşturur ve kaynağı değiştirmez. Soldan sağa okunur: önce "neyi koyacağız", sonra "nereden alacağız".
amounts = [100, 250, 400]
# normal döngü
with_markup = []
for a in amounts:
with_markup.append(a * 1.2)
# aynısı comprehension ile
with_markup = [a * 1.2 for a in amounts]
print(with_markup) # [120.0, 300.0, 480.0]
Türkçede "liste üreteci" olarak adlandırılsa da sonuç hazır bir listedir, üreteç değildir: tüm elemanlar hemen hesaplanır.
if ve if else ile liste üretimi
En çok karıştırılan konu budur. Filtre koşulu sona yazılır ve elemanları eler. Seçim koşulu (üçlü operatör) başa yazılır ve her elemanı iki değerden birine dönüştürür; hiçbir şeyi atmaz.
amounts = [100, 250, 400]
big = [a for a in amounts if a > 200] # [250, 400]
labels = ["çok" if a > 200 else "az" for a in amounts]
# ['az', 'çok', 'çok']
# filtre ve seçim birlikte
odd_marks = ["çift" if n % 2 == 0 else "tek" for n in range(6) if n > 2]
# ['tek', 'çift', 'tek']
[a for a in amounts if a > 200 else 0] yazımı sözdizimi hatasıdır: else yalnızca başta yazılan koşulla birlikte kullanılır.
İç içe liste üretimi ve iki boyutlu liste
Çift for, iç içe döngülerle aynı sırada yazılır, yukarıdan aşağıya. Liste içindeki listeleri birleştirirken sırayı karıştırmamanızı sağlar:
groups = [[1, 2], [3], [4, 5]]
flat = [x for g in groups for x in g]
# eşdeğeri:
# for g in groups:
# for x in g:
# flat.append(x)
print(flat) # [1, 2, 3, 4, 5]
İki boyutlu bir liste (matris) için comprehension içinde comprehension kullanılır. Klasik hata, listeyi çarparak satır oluşturmaktır: satırlar aynı nesne olur.
bad = [[0] * 3] * 2
bad[0][0] = 1
print(bad) # [[1, 0, 0], [1, 0, 0]] — iki satır da değişti
grid = [[0] * 3 for _ in range(2)]
grid[0][0] = 1
print(grid) # [[1, 0, 0], [0, 0, 0]]
# transpoz
m = [[1, 2, 3], [4, 5, 6]]
print([[row[i] for row in m] for i in range(3)]) # [[1, 4], [2, 5], [3, 6]]
Sözlük ve küme üretimi
Aynı yöntem diğer koleksiyonlar için de geçerlidir. İki nokta üst üste içeren süslü parantez bir sözlük (dict comprehension), içermeyen süslü parantez ise bir küme (set comprehension) oluşturur.
spent = [("yiyecek", 500), ("taksi", 300), ("yiyecek", 200)]
categories = {cat for cat, _ in spent}
print(categories) # {'yiyecek', 'taksi'} — kümenin sırası garanti değildir
last = {cat: amount for cat, amount in spent}
print(last) # {'yiyecek': 200, 'taksi': 300}
squares = {n: n ** 2 for n in range(4)} # {0: 0, 1: 1, 2: 4, 3: 9}
swapped = {v: k for k, v in squares.items()}
last örneğine dikkat edin: anahtar tekrar ettiğinde son değer kalır, 500'lük harcama kaybolur. Toplama yapmanız gerekiyorsa comprehension uygun değildir; döngü yazın ya da collections.Counter / defaultdict kullanın.
Demet üretimi var mı?
Python'da ayrı bir "tuple comprehension" yoktur. Parantez içindeki ifade bir üreteç ifadesi olur; demeti ise bunu tuple()'a vererek elde edersiniz:
t = tuple(n * 2 for n in range(3)) # (0, 2, 4)
Üreteç ifadesi: saklamadan üretmek
Köşeli parantezleri yuvarlak parantezle değiştirirseniz bir üreteç ifadesi elde edersiniz. Değerleri önceden hesaplamaz, istendikçe verir. Bir milyon eleman için bellek farkı çok büyüktür:
import sys
as_list = [n for n in range(1_000_000)]
as_gen = (n for n in range(1_000_000))
print(sys.getsizeof(as_list)) # yaklaşık 8 MB
print(sys.getsizeof(as_gen)) # birkaç yüz bayt
Üreteç ifadeleri çoğunlukla toplayıcı fonksiyonların içinde yaşar. Üreteç tek argümansa çevresindeki parantezleri yazmanız gerekmez. any() ve all() ilk cevabı bulunca durur, kalanı taramaz.
spent = [("yiyecek", 500), ("taksi", 300), ("yiyecek", 200)]
total = sum(a for _, a in spent) # 1000
has_big = any(a > 400 for _, a in spent) # True, 500'den sonrasına bakmaz
biggest = max(spent, key=lambda p: p[1]) # ('yiyecek', 500)
Seçim kuralı basittir: sonuç birden fazla kez gerekiyorsa, indeksleme ya da len() lazımsa liste kullanın. Bir kez dolaşıp bir şey hesaplayacaksanız üreteç kullanın.
Kapsam: değişken dışarı sızmaz
Deneyimli geliştiricileri bile yanıltan bir durum:
x = 10
squares = [x for x in range(3)]
print(x) # 10
Comprehension içindeki döngü değişkeni kendi kapsamında yaşar ve dıştaki değişkeni ezmez (Python 3'te böyledir; Python 2'de farklıydı). Normal bir for döngüsünde ise döngüden sonra x 2 olurdu. Python 3.12'den itibaren (PEP 709) comprehension'lar çevreleyen fonksiyona gömülerek daha hızlı çalışır, ama görünürlük kuralı aynı kalmıştır.
Python 3.15'te comprehension içinde * ve ** (PEP 798)
Açma (unpacking) için yıldız işareti fonksiyon çağrılarında ve liste literallerinde uzun süredir kullanılabiliyordu; ama comprehension içinde bu bir SyntaxError idi. Python 3.15'te bu kısıt kaldırıldı: *öğe, öğenin tüm elemanlarını oraya yerleştirmek anlamına gelir. Liste, küme ve üreteçlerde çalışır; asenkron olanlar dahil.
# Python 3.15+
groups = [[1, 2], [3], [4, 5]]
flat = [*g for g in groups] # [1, 2, 3, 4, 5]
uniq = {*g for g in groups} # küme
lazy = (*g for g in groups) # tembel üreteç
Çift yıldız da sözlükler için aynı işi görür. Bu, "varsayılan → dosya → bayraklar" biçimindeki katmanlı ayarlar için kullanışlıdır: her sonraki katman önceki katmanın anahtarlarını ezer.
# Python 3.15+
layers = [{"debug": False, "port": 80}, {"port": 8080}, {"debug": True}]
config = {**layer for layer in layers}
# {'debug': True, 'port': 8080}
Kursta vurgulanan iki not var. Yıldızı, parantez olmadan bir koşullu ifadeyle birleştiremezsiniz: [*w if w else 0 for w in weeks] sözdizimi hatasıdır; koşulu parantez içine almanız gerekir. Ayrıca bu kod 3.14 ve daha eski sürümlerde çalışmaz: kütüphaneniz eski sürümleri destekliyorsa çift for yapısında ya da itertools.chain.from_iterable ile kalın.
Asenkron üreteçler
async def fonksiyonunda yield varsa asenkron bir üreteç elde edersiniz. Onun üzerinde async for ile dolaşılır ve içeride ağ ya da disk işlemleri await ile beklenebilir. Asenkron liste üretimleri de vardır.
import asyncio
async def ticks(n):
for i in range(n):
await asyncio.sleep(0.1)
yield i
async def main():
result = [i async for i in ticks(3)]
print(result) # [0, 1, 2]
asyncio.run(main())
Tipik hatalar
- Üç katlı comprehension. İki
for, ikiifve bir üçlü operatör aynı satırda olursa bir hafta sonra kimse okuyamaz. Tek satıra sığmıyorsa normal bir döngü yazın: PEP 8 okunabilirliği kısalığın önüne koyar. - Tükenmiş üretecin ikinci kez kullanılması. Aynı üreteç üzerinde ikinci
sum(gen)0 döndürür, çünkü üreteç çoktan tükenmiştir. - Yan etki için comprehension.
[print(x) for x in xs]gereksiz birNonelistesi oluşturur. Eylemler içinfordöngüsü kullanın. - Üreteç ifadesinden liste beklemek.
(x for x in xs)[0]birTypeErrorverir: üreteçlerde indeks yoktur.
Pratik için üreteç görevleri
- Sonsuz Fibonacci sayıları üreten bir
fib()üreteci yazın veisliceile ilk 10 tanesini alın. - Üç harften uzun kelimeler için "kelime → uzunluk" sözlüğünü tek satırda üretin.
- Bir milyona kadar çift sayıların karelerinin toplamını, liste oluşturmadan hesaplayın.
- Bir listeyi
nelemanlık parçalara bölen birchunks(seq, n)üreteci yazın.
Özet
Yineleyici elemanları next() ile birer birer verir; üreteç bunu yazmanın en basit yoludur: yield içeren bir fonksiyon ya da parantez içinde bir ifade. Liste, sözlük ve küme üretimleri koleksiyonu tek satırda kurar; if filtresi sona, if else seçimi başa yazılır. Bellek tasarrufu ya da veri akışını bir kez işlemek için üreteç, sonucu birden fazla kullanmak için liste seçin. Üreteçler, comprehension'lar ve Python 3.15'in yeni açma sözdizimi kursta harcama takipçisi örneğiyle uygulamalı olarak işlenir.
Sık sorulan sorular
Üreteç fonksiyonunda return kullanabilir miyim?
Evet. Değersiz return üreteci sonlandırır; değerli return ise değeri StopIteration istisnasının value özelliğine koyar ve bunu yield from ifadesi alır.
Bir üreteci nasıl listeye çevirebilirim?
list() fonksiyonuna verin: list(gen). Bundan sonra üreteç tükenmiş olur ve tüm değerler bellekte bulunur.
List comprehension mi yoksa append kullanan döngü mü daha hızlıdır?
Genellikle comprehension biraz hızlıdır, çünkü her turda append metodunu çağırmaz. Ama seçim okunabilirliğe göre yapılmalı; fark nadiren belirleyici olur.
Bir nesnenin üreteç olup olmadığını nasıl anlarım?
Nesne için inspect.isgenerator(obj), fonksiyon için inspect.isgeneratorfunction(f) kullanın. Bir nesnenin yineleyici olduğunu isinstance(obj, collections.abc.Iterator) ile kontrol edebilirsiniz.
Üreteç ifadesi ile liste üretimi arasındaki fark nedir?
Köşeli parantez tüm listeyi hemen bellekte oluşturur; yuvarlak parantez ise elemanları istendikçe hesaplayan bir üreteç döndürür ve yalnızca bir kez dolaşılabilir.
Bu konu “Python 3.15: от новичка до профи” kursunda uygulamalı olarak ayrıntılı işleniyor.
Kursa git