Iteratory - prawie wszystko

Iteratory to kolejny powód, dla którego Python jest językiem tak pięknym i łatwym w użyciu. Umożliwiają często skrócenie do jednej linijki skomplikowanych konstrukcji z innych języków, jednocześnie nie tracąc, a wręcz zyskując na czytelności. Każdy programista Pythona zetknął się z iteratorami, choćby używając pętli for dla jakiegokolwiek typu kontenerowego. Nie każdy jednak wie, jakie inne iteratory można użyć, a także jak można nimi manipulować. Pora więc na małą syntezę tej wiedzy.

Typy wbudowane

Jak już wspomniałem wbudowane kontenery list, tuple, str, unicode, bytearray (zmienna wersja str) iterują po swojej zawartości od początku do końca. Kontenery set, frozenset - również. Choć nie zapewniają żadnej konkretnej kolejności, w jakiej zostaną zwrócone elementy. Ostatecznie dict iteruje po swoich kluczach. Również nie dając gwarancji kolejności.

Mniej znanym jako iterator typem jest file, który uzyskujemy dzięki funkcji open. Kod:

with open('iteratory.rst', 'r') as f:
    for l in f:
        print l,

Wyświetli nam zawartość pliku iteratory.rst (ponieważ iterator zostawia znaki końca linii, daliśmy na końcu przecinek, który powoduje, że print nie dodaje następnego). Tę funkcjonalność obiektu file można użyć np. do napisania zgrabnego kodu parsującego pliki.

Wśród wbudowanych funkcji Pythona mamy też ciekawą funkcję iter. O ile wywołanie jej z jednym iterowalnym argumentem spowoduje po prostu stworzenie iteratora, o tyle w przypadku dwóch argumentów umożliwia nam zbudowanie iteratora na podstawie funkcji (lub innego obiektu wywoływalnego). Na przykład poniższy kod:

def get_new_num():
    ans = raw_input('Podaj kolejna liczbe (0 by zakonczyc): ')
    return int(ans)

result = []
for l in iter(get_new_num, 0):
    result.append(l)

print result

Będzie wczytywał z konsoli kolejne liczby, aż użytkownik poda 0, a następnie wypisze ich listę na ekranie. W tym przypadku funkcji iter podaliśmy funkcję get_new_num (bez nawiasów, bo chcemy, by iter pracowała na funkcji, a nie na jej wyniku) oraz liczbę 0 jako "strażnika", który zakończy działanie iteratora.

Ostatnim typem jaki chcę przedstawić jest wynik funkcji xrange(). Różni się on od dobrze znanej wszystkim range() tym, że nie tworzy w pamięci listy. Użycie go zamiast range() w pętli for nie robi żadnej (oprócz wydajnościowych) różnicy. W Pythonie 3.x funkcja range() działa już w ten sposób. W serii 2.x zostawiono stare działanie biorąc pod uwagę, że niektórzy użytkownicy mogli w starym kodzie zrobić z wynikiem range() coś więcej niż tylko go przeiterować.

Komprehensje iteratorów

Komprehensje to również konstrukcje dość programistom Pythona znane. Najczęściej jednak są to komprehensje list. Na przykład wyrażenie:

[x**2 for x in xrange(1, 11)]

ma wartość [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]. Jeśli jednak zastąpimy nawiasy kwadratowe okrągłymi uzyskamy obiekt, który ma się do tej listy tak, jak iterator xrange() do listy uzyskanej dzięki range(). Komprehensje iteratorów obliczane są w sposób leniwy, co możemy zobaczyć na przykładzie:

from random import randint

def get_random_num():
    print 'Losuje kolejna...'
    return randint(1, 6)

for i in [get_random_num() for i in range(3)]:
    print i

for i in (get_random_num() for i in range(3)):
    print i

Po uruchomieniu tego programu zobaczymy, że w przypadku pierwszej pętli losowanie liczb zostaje wykonane przez rozpoczęciem iteracji (w sposób zachłanny), natomiast w przypadku drugiej każda wartość losowana jest dopiero przed jej wyświetleniem (w sposób leniwy). Dodatkowo komprehensja iteratora nie zajmuje generowanymi wartościami pamięci, co może mieć kluczowe znaczenie przy pracy na dużych zbiorach danych.

Funkcje generatory

Funkcja generator to taka funkcja, w której ciele użyto słowa kluczowego yield. Funkcja taka jeśli posiada słowo kluczowe return, to nie może za nim stać żadne wyrażenie. Interpreter Python traktuje taką funkcję bardzo dziwacznie. Zawija ją w iterator, a następnie sprawia, że wywołanie tej funkcji zwraca tenże iterator, który przy każdej iteracji wywołuje samą tą funkcję do momentu natrafienia na yield. Wówczas iterator zwraca wartość wyrażenia podanego za yield i zawiesza działanie funkcji w tym miejscu. Brzmi to strasznie zawile, ale jest bardzo intuicyjne w użyciu. Następujący kod:

def bum(max):
    i = 1
    while True:
        if i > max:
            return
        if '3' in str(i) or not i % 3:
            yield 'BUM!'
        else:
            yield str(i)
        i += 1

for i in bum(50):
    print i

Gra w podwórkową zabawę w "BUM!". Dla każdej liczby zawierające 3 lub podzielnej przez 3 wyświetla bum, a dla innych - samą liczbę. Użyłem pętli while zamiast for, aby pokazać, jak można użyć w generatorze słowa kluczowego return.

Interfejs iteratora

A co jeśli tego wszystkiego co powyżej opisano jest nam za mało? Wówczas pozostaje nam stworzyć własny obiekt iterowalny. Aby obiekt działał jako iterator musimy spełnić jeden z warunków:

  1. Napisać dla niego metodę __iter__(), która zwróci obiekt posiadający next() (zwracający kolejne wartości).
  2. Napisać dla niego metodę __getitem__ i upewnić się, że działa ona dla kolejnych wartości od 0. Każdy więc obiekt indeksowalny liczbami od 0 w górę jest też iterowalny.

Jeśli spełnimy oba te warunki, priorytet ma pierwszy. Poniższy iterator działa na zasadzie sita Eratostenesa:

class PrimeIter:
    def __init__(self, max = None):
        self.max = max
        self.current = 2
        self.found = []

    def __iter__(self):
        return self

    def next(self):
        while True:
            for i in self.found:
                if not self.current % i:
                    break
            else:
                self.found.append(self.current)
                return self.current
            self.current += 1
            if self.max and self.current > self.max:
                raise StopIteration

for i in PrimeIter(100):
    print i

Choć możnaby go też napisać w oparciu o funkcję generator, to jednak stworzenie takiej klasy może być uznane za czytelniejsze. Zauważmy, że aby zakończyć działanie takiego iteratora używamy wyjątku StopIteration

Operacje na iteratorach

Wbudowana funkcja enumerate dodaje kolejne wartości całkowite do naszego iteratora tworząc dwuelementowe krotki (tuple). Na przykład kod:

with open('iteratory.rst', 'r') as f:
    for line_tuple in enumerate (f, 1):
        print '%i\t%s' % linetuple,

Wyświetli nam zawartość pliku iteratory.rst z numerami linii.

Moduł itertools zawiera wiele ciekawych funkcji, z których jednak część może być zastąpiona komprehensjami. Część z nich jest iteratorowym odpowiednikiem funkcji wbudowanych. Np. ifilter() to leniwy odpowiednik funkcji filter()

from itertools import ifilter
for i in ifilter(lambda x: x%2, range(10)):
    print i

Wyświetli nam liczby nieparzyste mniejsze od 10. Możemy jednak zapisać to także:

for i in (x for x in range(10) if x%2):
    print i

Działanie obu tych kodów jest identyczne, wybór przystępniejszego pozostawiam programiście.

Tekst ten oczywiście nie wyczerpuje tematyki. Zachęcam więc wszystkich, aby:

  1. Poznali moduł itertools
  2. Zapoznając się z nowymi bibliotekami sprawdzali, czy dostarczane przez nie obiekty można używać jako iteratory
  3. Zastanowili się, jak ich własny kod można upiększyć i usprawnić (szybsze działanie, mniejsze zużycie pamięci) przez zastąpienie kontenerów leniwymi iteratorami.

Do zobaczenia w drugiej części tutorialu społecznościówkowego.

Meta

Opublikowany: Lut. 19, 2011

Autor: zefciu

Komentarze:  

Liczba słów: 1,209

Następny: Społecznościówka w 3 krokach - krok 2 - uwierzytelnianie

Poprzedni: Społecznościówka w 3 krokach - krok 1

Bookmark and Share

Tagi

iteratory komprehensje leniwe pętle wydajność zachłanne

Linki z wpisu

  1. 9.7. itertools — Functions creating iterators for efficient looping — ...
Comments powered by Disqus