Generatorler, üreteç işlevleri (örneğin yield kullanarak) veya üreteç ifadeleri ((ifade for x in iterator) kullanarak) tarafından oluşturulan tembel yineleyicilerdir.
- yield
<expr>
- yield from
<expr>
<var>
= yield<expr>
- next(
<iter>
)
Generator ifadeleri liste, sözlük ve küme comprehension’lara benzer, ancak parantez içine alınır. Bir fonksiyon çağrısı için tek argüman olarak kullanıldıklarında parantezlerin mevcut olması gerekmez.
ifade = (x**2 for x in range(10))
Bu örnek, 0 (x = 0) dahil olmak üzere ilk 10 mükemmel kareyi üretir.
Generator fonksiyonları, gövdelerinde bir veya daha fazla yield deyimi olması dışında normal fonksiyonlara benzer. Bu tür fonksiyonlar herhangi bir değer döndüremez (ancak üreteci erken durdurmak istiyorsanız boş dönüşlere izin verilir).
def function(): for x in range(10): yield x**2
Bu generator fonksiyonu önceki generator ifadesine eşdeğerdir, aynı çıktıyı verir.
Not: tüm generator ifadelerinin kendi eşdeğer fonksiyonları vardır, ancak tersi geçerli değildir.
Aksi takdirde her iki parantez de tekrarlanacaksa, bir generator ifadesi parantezler olmadan kullanılabilir:
sum(i for i in range(10) if i % 2 == 0) #Çıktı: 20
any(x = 0 for x in foo) #Çıktı: foo'ya bağlı olarak True veya False
type(a > b for a in foo if a % 2 == 1) #Çıktı: <class 'generator'>
Bunun yerine:
sum((i for i in range(10) if i % 2 == 0))
any((x = 0 for x in foo))
type((a > b for a in foo if a % 2 == 1))
Ama şu şekilde değil:
fooFunction(i for i in range(10) if i % 2 == 0,foo,bar)
return x = 0 for x in foo
barFunction(baz, a > b for a in foo if a % 2 == 1)
Bir üreteç işlevi çağrıldığında, daha sonra üzerinde yineleme yapılabilecek bir generator nesnesi üretilir. Diğer yineleyici türlerinin aksine, generator nesneleri üzerinde yalnızca bir kez dolaşılabilir.
g1 = function()
print(g1) # Çıktı: <generator object function at 0x1012e1888>
Bir generator’ın gövdesinin hemen çalıştırılmadığına dikkat edin: yukarıdaki örnekte function() işlevini çağırdığınızda, ilk print deyimini bile çalıştırmadan hemen bir generator nesnesi döndürür. Bu, üreteçlerin liste döndüren işlevlerden daha az bellek tüketmesini ve sonsuz uzunlukta diziler üreten üreteçler oluşturmasını sağlar.
Bu nedenle, üreteçler genellikle veri biliminde ve büyük miktarda veri içeren diğer bağlamlarda kullanılır. Diğer bir avantajı ise diğer kodların, dizinin tamamının üretilmesini beklemeden bir üreteç tarafından üretilen değerleri hemen kullanabilmesidir.
Ancak, bir üreteç tarafından üretilen değerleri birden fazla kez kullanmanız gerekiyorsa ve bunları üretmek saklamaktan daha maliyetliyse, diziyi yeniden üretmektense üretilen değerleri bir liste olarak saklamak daha iyi olabilir. Daha fazla ayrıntı için aşağıdaki ‘Bir üreteci sıfırlama’ bölümüne bakın.
Tipik olarak bir üreteç nesnesi bir döngüde veya yinelenebilir bir nesne gerektiren herhangi bir işlevde kullanılır:
for x in g1:
print("Alındı", x)
# Çıktı:
# Alınan 0
# Alındı 1
# Alındı 4
# Alındı 9
# Alındı 16
# Alındı 25
# Alındı 36
# Alındı 49
# Alındı 64
# Alındı 81
arr1 = liste(g1)
# arr1 = [], çünkü yukarıdaki döngü zaten tüm değerleri tüketmiştir.
g2 = function()
arr2 = list(g2) # arr2 = [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
Üreteç nesneleri yineleyici olduğundan, next() işlevi kullanılarak bunlar üzerinde manuel olarak yineleme yapılabilir. Bunu yapmak, sonraki her çağrıda elde edilen değerleri tek tek döndürecektir.
Kaputun altında, bir üreteçte next() işlevini her çağırdığınızda, Python bir sonraki yield deyimine ulaşana kadar üreteç işlevinin gövdesindeki deyimleri yürütür. Bu noktada yield komutunun argümanını döndürür ve bunun gerçekleştiği noktayı hatırlar. next() bir kez daha çağrıldığında, yürütme o noktadan devam eder ve bir sonraki yield deyimine kadar devam eder.
Python daha fazla yield ile karşılaşmadan üreteç işlevinin sonuna ulaşırsa, bir StopIteration istisnası yükseltilir (bu normaldir, tüm yineleyiciler aynı şekilde davranır).
g3 = function()
a = next(g3) # a 0 olur
b = next(g3) # b 1 olur
c = next(g3) # c 2 olur
...
j = next(g3) # StopIteration'ı yükseltir, j tanımsız kalır
Python 2’de üreteç nesnelerinin, elde edilen değerleri manuel olarak yinelemek için kullanılabilecek .next() yöntemlerine sahip olduğunu unutmayın. Python 3’te bu yöntem tüm yineleyiciler için .__next__() standardı ile değiştirildi.
Bir generatorün sıfırlanması
Bir üreteç tarafından üretilen nesneler arasında yalnızca bir kez yineleme yapabileceğinizi unutmayın. Bir koddaki nesneleri zaten yinelediyseniz, başka bir girişimde bulunmanız durumunda None sonucunu alırsınız.
Bir üreteç tarafından üretilen nesneleri birden fazla kez kullanmanız gerekiyorsa, üreteç işlevini yeniden tanımlayabilir ve ikinci kez kullanabilirsiniz. Ya da alternatif olarak, üreteç işlevinin çıktısını ilk kullanımda bir listede saklayabilirsiniz. Büyük hacimli verilerle uğraşıyorsanız ve tüm veri öğelerinin bir listesini saklamak çok fazla disk alanı kaplayacaksa, üreteç işlevini yeniden tanımlamak iyi bir seçenek olacaktır. Tersine, öğeleri başlangıçta oluşturmak maliyetliyse, oluşturulan öğeleri bir listede saklamayı tercih edebilirsiniz, böylece bunları yeniden kullanabilirsiniz.
Sonsuz diziler
Üreteçler sonsuz dizileri temsil etmek için kullanılabilir:
def integers_starting_from(n):
while True:
yield n
n += 1
natural_numbers = integers_starting_from(1)
Yukarıdaki gibi sonsuz sayı dizisi itertools.count yardımıyla da oluşturulabilir. Yukarıdaki kod aşağıdaki gibi yazılabilir:
natural_numbers = itertools.count(1)
Yeni üreteçler üretmek için sonsuz üreteçler üzerinde üreteç comprehension’larını kullanabilirsiniz:
multiples_of_two = (x * 2 for x in natural_numbers)
multiples_of_three = (x for x in natural_numbers if x % 3 == 0)
Sonsuz bir üretecin bir sonu olmadığını unutmayın, bu nedenle üreteci tamamen tüketmeye çalışacak herhangi bir işleve geçirmenin korkunç sonuçları olacaktır:
list(multiples_of_two) # asla sonlandırılmayacak veya işletim sistemine özgü bir hata verecektir
Bunun yerine, range (veya python < 3.0 için xrange) ile liste/set kavramalarını kullanın:
first_five_multiples_of_three = [next(multiples_of_three) for _ in range(5)]
# [3, 6, 9, 12, 15]
veya yineleyiciyi bir alt kümeye dilimlemek için itertools.islice() işlevini kullanın:
from itertools import islice
multiples_of_four = (x * 4 for x in integers_starting_from(1))
first_five_multiples_of_four = list(islice(multiples_of_four, 5))
# [4, 8, 12, 16, 20]
Aynı “kökten” gelen diğer tüm üreteçler gibi orijinal üretecin de güncellendiğini unutmayın:
next(natural_numbers) # 16 verir
next(multiples_of_two) # 34 verir
next(multiples_of_four) # 24 verir
Sonsuz bir dizi, bir for döngüsü ile de yinelenebilir. Döngünün sonunda sonlandırılması için koşullu bir break deyimi eklediğinizden emin olun:
for idx, number in enumerate(multiplies_of_two):
print(number)
if idx == 9:
break # ikisinin ilk 10 çarpımını aldıktan sonra durun
Klasik örnek – Generatörler ile Fibonacci sayıları türetme
import itertools
def fibonacci():
a, b = 1, 1
while True:
yield a
a, b = b, a + b
first_ten_fibs = list(itertools.islice(fibonacci(), 10))
# [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
def nth_fib(n):
return next(itertools.islice(fibonacci(), n - 1, n))
ninety_nineth_fib = nth_fib(99) # 354224848179261915075
Nesneleri üretece gönderme
Bir üreteçten değer almanın yanı sıra, send() yöntemini kullanarak bir üretece nesne göndermek de mümkündür.
def accumulator():
total = 0
value = None
while True:
# gönderilen değeri al
value = yield total
if value is None: break
# değerleri topla
total += value
generator = accumulator()
# ilk " yield" a kadar ilerleyin
next(generator) # 0
# Bu noktadan sonra, üreteç değerleri toplar
generator.send(1) # 1
generator.send(10) # 11
generator.send(100) # 111
# ...
# next(generator) çağrısı, generator.send(None) çağrısına eşdeğerdir
next(generator) # StopIteration
Burada olan şey şudur:
- next(generator) öğesini ilk çağırdığınızda, program ilk yield deyimine ilerler ve bu noktada toplam değerini döndürür, yani 0. üretecin yürütülmesi bu noktada askıya alınır.
- Daha sonra generator.send(x) öğesini çağırdığınızda, yorumlayıcı x argümanını alır ve onu son yield deyiminin dönüş değeri haline getirir ve bu değer value öğesine atanır. Ardından üreteç, bir sonraki değeri verene kadar her zamanki gibi devam eder.
- Son olarak next(generator) dediğinizde, program bunu üretece None gönderiyormuşsunuz gibi değerlendirir. None ile ilgili özel bir şey yoktur, ancak bu örnek None değerini üreteçten durmasını istemek için özel bir değer olarak kullanır.
Başka bir yinelenebilirden tüm değerleri elde etme
Başka bir yinelenebilirden tüm değerleri elde etmek istiyorsanız yield from kullanın:
def foob(x):
yield from range(x * 2)
yield from range(2)
list(foob(5)) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1]
Bu üreteçler için de geçerlidir.
def fibto(n):
a, b = 1, 1
while True:
if a >= n: break
yield a
a, b = b, a + b
def usefib():
yield from fibto(10)
yield from fibto(20)
list(usefib()) # [1, 1, 2, 3, 5, 8, 1, 1, 2, 3, 5, 8, 13]
Yinelemeler
Bir üreteç nesnesi yineleyici protokolünü destekler. Yani, yürütülmesinde adım adım ilerlemek için kullanılan bir next() yöntemi (Python 3.x’te __next__()) sağlar ve __iter__ yöntemi kendisini döndürür. Bu, bir üretecin genel yinelenebilir nesneleri destekleyen herhangi bir dil yapısında kullanılabileceği anlamına gelir.
#Python 2.x xrange() işlevinin kısmi bir uygulaması
def xrange(n):
i = 0
while i < n:
yield i
i += 1
# döngü
for i in xrange(10):
print(i) # 0, 1, ..., 9 değerlerini yazdırır
# paketten çıkarma
a, b, c = xrange(3) # 0, 1, 2
# bir liste oluşturmak
l = liste(xrange(10)) # [0, 1, ..., 9]
next fonksiyonu
next() yerleşik işlevi, herhangi bir yineleyiciden (üretici yineleyici dahil) bir değer almak ve yineleyicinin tükenmesi durumunda varsayılan bir değer sağlamak için kullanılabilen kullanışlı bir sarmalayıcıdır.
def nums():
yield 1
yield 2
yield 3
generator = nums()
next(generator, None) # 1
next(generator, None) # 2
next(generator, None) # 3
next(generator, None) # None
next(generator, None) # None
# ...
Sözdizimi next(iterator[, default]) şeklindedir. Yineleyici sona ererse ve varsayılan bir değer aktarılmışsa, bu değer döndürülür. Varsayılan değer verilmemişse, StopIteration yükseltilir.
Coroutine’ler
Üreteçler, coroutine’leri uygulamak için kullanılabilir:
#üreteç oluşturma ve ilk yield'a ilerletme
def coroutine(func):
def start(*args,**kwargs):
cr = func(*args,**kwargs)
next(cr)
return cr
return start
# örnek coroutine
@coroutine
def adder(sum = 0):
while True:
x = yield sum
sum += x
# örnek kullanım
s = adder()
s.send(1) # 1
s.send(2) # 3
Coroutine’ler durum makinelerini uygulamak için yaygın olarak kullanılır, çünkü öncelikle düzgün çalışması için bir durum gerektiren tek yöntemli prosedürler oluşturmak için kullanışlıdırlar. Mevcut bir durum üzerinde çalışırlar ve işlem tamamlandığında elde edilen değeri döndürürler.
Liste oluşturma kodunun yeniden düzenlenmesi
Boş bir listeyle başlayıp bu listeye tekrar tekrar ekleme yaparak bir liste oluşturan ve döndüren karmaşık bir kodunuz olduğunu varsayalım:
def create():
result = []
# iş mantığı buraya...
result.append(value) # muhtemelen birkaç yerde
# daha fazla mantık...
return result # muhtemelen birkaç yerde
values = create()
İç mantığı bir liste kavrama ile değiştirmek pratik olmadığında, tüm işlevi yerinde bir oluşturucuya dönüştürebilir ve ardından sonuçları toplayabilirsiniz:
def create_gen():
# mantık...
yield value
# daha fazla mantık
return # fonksiyonun sonunda ise gerekli değildir, elbette
values = list(create_gen())
Mantık özyinelemeli ise, özyinelemeli çağrıdan gelen tüm değerleri “düzleştirilmiş” bir sonuca dahil etmek için yield from kullanın:
def preorder_traversal(node):
yield node.value
for child in node.children:
yield from preorder_traversal(child)
Bir dizindeki tüm dosyaları özyinelemeli olarak listeleyen yield
İlk olarak, dosyalarla çalışan kütüphaneleri içe aktarın:
from os import listdir
from os.path import isfile, join, exists
Bir dizinden yalnızca dosyaları okumak için bir yardımcı işlev:
def get_files(path):
for file in listdir(path):
full_path = join(path, file)
if isfile(full_path):
if exists(full_path):
yield full_path
Yalnızca alt dizinleri almak için başka bir yardımcı işlev:
def get_directories(path):
for directory in listdir(path):
full_path = join(path, directory)
if not isfile(full_path):
if exists(full_path):
yield full_path
Şimdi bir dizin ve tüm alt dizinleri içindeki tüm dosyaları özyinelemeli olarak almak için bu işlevleri kullanın (üreteçleri kullanarak):
def get_files_recursive(directory):
for file in get_files(directory):
yield file
for subdirectory in get_directories(directory):
for file in get_files_recursive(subdirectory): # burada özyinelemeli çağrı
yield file
Bu fonksiyon yield from kullanılarak basitleştirilebilir:
def get_files_recursive(directory):
yield from get_files(directory)
for subdirectory in get_directories(directory):
yield from get_files_recursive(subdirectory)
kısaca generator ifadeleri
Comprehension benzeri bir sözdizimi kullanarak üreteç yineleyicileri oluşturmak mümkündür.
generator = (i * 2 for i in range(3))
next(generator) # 0
next(generator) # 2
next(generator) # 4
next(generator) # StopIteration'ı yükseltir
Bir fonksiyona mutlaka bir liste aktarılması gerekmiyorsa, bir fonksiyon çağrısının içine bir üreteç ifadesi yerleştirerek karakterlerden tasarruf edebilir (ve okunabilirliği artırabilirsiniz). İşlev çağrısındaki parantez, ifadenizi dolaylı olarak bir üretici ifadesi haline getirir.
sum(i ** 2 for i in range(4)) # 0^2 + 1^2 + 2^2 + 3^2 = 0 + 1 + 4 + 9 = 14
Ayrıca, üzerinde yinelediğiniz listenin tamamını yüklemek yerine (yukarıdaki örnekte [0, 1, 2, 3]), üreteç Python’un değerleri gerektiği gibi kullanmasına izin verdiği için bellekten tasarruf edersiniz.
Fibonacci Sayılarını bulmak için bir üreteç kullanma
Bir üretecin pratik bir kullanım durumu, sonsuz bir serinin değerleri arasında yineleme yapmaktır. İşte Fibonacci Dizisinin ilk on terimini bulmak için bir örnek.
def fib(a=0, b=1):
"""Fibonacci sayıları üreten üreteç. a` ve `b` tohum değerleridir"""
while True:
yield a
a, b = b, a + b
f = fib()
print(', '.join(str(next(f)) for _ in range(10)))
0, 1, 1, 2, 3, 5, 8, 13, 21, 34
Arama
next işlevi yineleme olmadan da kullanışlıdır. Bir üreteç ifadesini next’e aktarmak, bazı yüklemlerle eşleşen bir öğenin ilk oluşumunu aramanın hızlı bir yoludur. Aşağıdaki gibi prosedürel kod
def find_and_transform(sequence, predicate, func):
for element in sequence:
if predicate(element):
return func(element)
raise ValueError
item = find_and_transform(my_sequence, my_predicate, my_func)
ile değiştirilebilir:
item = next(my_func(x) for x in my_sequence if my_predicate(x))
# Hiçbir eşleşme yoksa StopIteration yükseltilecektir; bu istisna
# İstenirse yakalanabilir ve dönüştürülebilir.
Bu amaçla, istisnayı dönüştürmek için first = next gibi bir takma ad veya bir sarmalayıcı işlev oluşturmak istenebilir:
def first(generator):
try:
return next(generator)
except StopIteration:
raise ValueError
Paralel olarak üreteçler üzerinde yineleme
Birden fazla üreteci paralel olarak yinelemek için zip yerleşik bileşenini kullanın:
for x, y in zip(a,b):
print(x,y)
Sonuç:
1 x 2 y 3 z
Python 2’de bunun yerine itertools.izip kullanmalısınız. Burada ayrıca tüm zip fonksiyonlarının tuple ürettiğini görebiliriz.
Zip’in yinelenebilir öğelerden biri biter bitmez yinelemeyi durduracağını unutmayın. En uzun yinelenebilir öğe kadar yinelemek isterseniz, itertools.zip_longest() işlevini kullanın.
Birden fazla yineleyiciyi birlikte zincirleme
Sırayla birkaç üreteçten değerler üretecek tek bir üreteç oluşturmak için itertools.chain kullanın.
from itertools import chain
a = (x for x in ['1', '2', '3', '4'])
b = (x for x in ['x', 'y', 'z'])
' '.join(chain(a, b))
Sonuç:
'1 2 3 4 x y z'
Alternatif bir constructor olarak, tek parametre olarak iterable’ların iterable’ını alan chain.from_iterable classmethod’unu kullanabilirsiniz. Yukarıdaki ile aynı sonucu elde etmek için:
' '.join(chain.from_iterable([a,b])
chain rastgele sayıda argüman alabilirken, chain.from_iterable sonsuz sayıda yinelenebilir dosyayı zincirlemenin tek yoludur.