koddla

Yazılımcıları bilgi ile güçlendirir.

Python’da bir nesnenin yinelenebilir olduğu nasıl anlaşılır?

Elimde bir obje olsun ve bu objenin yinelenebilir olup olmadığını merak ettiğimi varsayalım. Bunun için en uygun yöntem objenin yinelenebilir olduğunu varsaymak, eğer bu çalışmazsa düzgün bir şekilde başarısız olmaktır:

try:
    iter(belki_yinelenebilir)
    print('yineleme muhtemelen çalışır')
except TypeError:
    print('yinelenemez')

Peki neden böyle? iter, __iter__, __getitem__ fonksiyonlarının etkileşimine ve perdenin arkasında neler olduğuna bakalım. Bu bilgiyle donanarak, yapabileceğiniz en iyi şeyin neden yukarıdaki olduğunu anlayacağız.

Önce elimizdeki gerçekleri listeleyelim. Ardından Python’da bir döngü çalıştırdığınızda ne olduğuna hızlı bir şekilde bakalım, sonrada bu gerçekleri örneklendirelim.

Gerçekler

  1. Aşağıdaki koşullardan en az biri doğruysa o objesi için iter(o) ile herhangi bir nesneden yineleme alabilirsiniz:

    a) o nesnesi yineleyici döndüren bir __iter__ metoduna sahiptir. Yineleyici, __iter__ ve __next__ (python 2’de next) metotlarını içeren herhangi bir nesne olmalı.

    b) o, __getitem__ metoduna sahiptir.
  2. Bir nesnenin Iterable ve Sequence örneği olması veya __iter__ özniteliğini denetlemek yeterli değildir
  3. Eğer o nesnesi sadece __getitem__ metoduna sahipse ve __iter__ yoksa, iter(o), 0’dan başlayarak tamsayı dizininden öğeleri getirmeye çalışan bir yineleyici oluşturur. Yineleyici, önce IndexError hatası verir, sonrasında ise StopIteration.
  4. En genel anlamda, iter ile geri dönen yineleyicinin doğru olduğunu denemek dışında kontrol etmenin bir yolu yoktur.
  5. __iter__ kazanır. Bir nesne __iter__ ve __getitem__ metotlarının her ikisine de sahipse iter(o), __iter__‘i çağırı.
  6. Kendi nesnelerinizi yinelemeli hale getirmek istiyorsanız, her zaman __iter__ metodunu uygulamalısınız.

for Döngüsü

Devam etmeden önce Python’da bir döngü çalıştırdığınızda ne olacağını anlamanız gerekir.

Bir yenilenebilir o nesnesi için for item in o yinelemesi kullanıldığında, Python iter(o) metodunu çalıştırır ve bir yineleyici nesne dönmesini bekler. Yineleyici, __iter__ ve __next__ (python 2’de next) metotlarını içeren herhangi bir nesnedir.

Kural gereği, bir yineleyicinin __iter__ yöntemi nesnenin kendisini döndürmelidir (yani return self). Python daha sonra StopIteration hatası dönene kadar yineleyicide next‘i çağırır. Tüm bunlar örtülü olarak gerçekleşir, ancak aşağıdaki örnek bunu görünür hale getirir:

import random

class DemoIterable(object):
    def __iter__(self):
        print('__iter__ called')
        return DemoIterator()

class DemoIterator(object):
    def __iter__(self):
        return self

    def __next__(self):
        print('__next__ called')
        r = random.randint(1, 10)
        if r == 5:
            print('raising StopIteration')
            raise StopIteration
        return r

DemoIterable üzerinde yineleme:

>>> di = DemoIterable()
>>> for x in di:
...     print(x)
...
__iter__ called
__next__ called
9
__next__ called
8
__next__ called
10
__next__ called
3
__next__ called
10
__next__ called
raising StopIteration

Tartışma ve Örnekler

1. ve 2.

Aşağıdaki sınıfı göz önünde bulundurun:

class BasicIterable(object):
    def __getitem__(self, item):
        if item == 3:
            raise IndexError
        return item

iter‘i çağırmak, yinelemeyi BasicIterable üzerinden sorunsuz bir şekilde döndürür, çünkü BasicIterable __getitem__ metoduna sahip.

>>> b = BasicIterable()
>>> iter(b)
<iterator object at 0x7f1ab216e320>

Ancak b, __iter__‘e sahip olmadığından Iterable veya Sequence örneği olarak kabul edilmez.

>>> from collections import Iterable, Sequence
>>> hasattr(b, '__iter__')
False
>>> isinstance(b, Iterable)
False
>>> isinstance(b, Sequence)
False

Bu nedenle, Luciano Ramalho’nun akıcı Python‘unda, bir nesnenin yinelenebilir olup olmadığını kontrol etmenin en doğru yolu olarak o nesne üzerinde yinelenme yapmayı ve işlemeyi önerir. 

3. noktada: Yalnızca __getitem__sağlayan nesneler üzerinde yineleme, __iter__ değil

BasicIterable örneği üzerinde yineleme beklendiği gibi çalışır: Python, bir IndexError hatası alana kadar sıfırdan başlayarak dizine göre öğeleri getirmeye çalışan bir yineleme oluşturur. Demo nesnesinin __getitem__ metodu, item‘i döndürür.

>>> b = BasicIterable()
>>> it = iter(b)
>>> next(it)
0
>>> next(it)
1
>>> next(it)
2
>>> next(it)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Yineleyicinin bir sonraki öğeyi döndüremediğinde StopIteration hatası verir ve IndexError içeride halledilir.

>>> for x in b:
...     print(x)
...
0
1
2

4: iter__iter__ çağrıldığında yinelemeyi kontrol eder:

iter(o) ile bir nesne için çağrıldığında, eğer __iter__ yöntemi varsa, dönüş değerinin bir yinelemeci olduğundan emin olmak ister. Bu, döndürülen nesnenin __next__‘i uygulaması gerektiği anlamına gelir. iter yalnızca __getitem__‘i sağlayan nesneler için herhangi bir denetim gerçekleştiremez, çünkü nesnenin öğelerine sayı indexi ile erişilebilir olup olmadığını denetlemenin bir yolunu bulamaz.

class FailIterIterable(object):
    def __iter__(self):
        return object() # not an iterator

class FailGetitemIterable(object):
    def __getitem__(self, item):
        raise Exception

FailIterIterable örneği başlar başlamaz hata verirken, FailGetItemIterable ancak ilk __next__ çağrısında hata verir.

>>> fii = FailIterIterable()
>>> iter(fii)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: iter() returned non-iterator of type 'object'
>>>
>>> fgi = FailGetitemIterable()
>>> it = iter(fgi)
>>> next(it)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/path/iterdemo.py", line 42, in __getitem__
    raise Exception
Exception

6. noktada: __iter__ kazandı

Bu çok açık. Bir nesne __iter__ ve __getitem__‘e sahipse iter, __iter__‘i çağırır. Aşağıdaki sınıfı göz önünde bulundurun:

class IterWinsDemo(object):
    def __iter__(self):
        return iter(['__iter__', 'wins'])

    def __getitem__(self, item):
        return ['__getitem__', 'wins'][item]

ve bir örnek:

>>> iwd = IterWinsDemo()
>>> for x in iwd:
...     print(x)
...
__iter__
wins

7. noktada: yinelemeli sınıflarınız __iter__‘e sahip olmalı

Kendinize neden list gibi yerleşik bir çok dizinin __iter__’i implement ettiğini sorabilirsiniz. Özellikle __getitem__ yeterli olacakken:

class WrappedList(object): # note: no inheritance from list!
    def __init__(self, lst):
        self._list = lst

    def __getitem__(self, item):
        return self._list[item]

Sonuçta, yukarıdaki sınıfın örnekleri üzerinde yineleme __getitem__‘i çağıracak.

>>> wl = WrappedList(['A', 'B', 'C'])
>>> for x in wl:
...     print(x)
... 
A
B
C

Özel yinelemelerinizin neden __iter__‘i uygulanması gerektiğine dair nedenler şunlar:

  1. __iter__‘i uygularsanız, örnekler yinelenebilir olarak kabul edilir ve isinstance(o, collections.abc.Iterable) True döner.
  2. __iter__ tarafından döndürülen nesne bir yineleyici değilse, hemen başarısız olur ve bir TypeError hatası verir.
  3. __getitem__’in kullanımı geriye dönük uyumluluk sağlar. 

Bir cevap yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

Back to top