05. Итератори и генератори

05. Итератори и генератори

05. Итератори и генератори

23 март 2015

Въпрос

Какво се случва, когато напишем object.attr?

  1. Python връща object.__dict__['attr']
  2. Ако няма такъв, Python търси в object.__class__, ако това е функция, се връща специален обект (bound method), на който може да извикате ().
  3. Ако това в object.__class__ не е функция, то просто се връща
  4. Ако го няма там, се вика object.__getattr__('attr')

Въпрос с код

class A:
    pass

def __str__(something):
    return "hax0r"

a = A()
a.__str__ = __str__

print(a)

Що е то итериране

Обикновено колекциите съдържат данни, които искаме да обхождаме.

Понякога тези данни не са подредени и няма начин да ги достъпваме директно.(set-ове, dict-ове)

Как можем да итерираме

с for

for cheese in cheeses:
    print('-{0}?'.format(cheese))
    print('-No...')

Какво можем да итерираме

Итеруеми

многократно vs. еднократно

Някои обекти могат да се итерират многократно(списъци, множества...). Но не всички.

squares = map(lambda x: x ** 2, range(5))
for number in squares:
    print(number)
# 0 1 4 9 16 

for number in squares:
    print(number)
#

Обикновено мързеливите се итерират по веднъж.

__iter__

Индексирането не винаги има смисъл, въпреки че обекта може да се итерира

__iter__ Връща обект-итератор, с който можем да обходим нашата "колекция"

Итераторът е обект, пазещ позицията на текущо обхождане на колекция

(обект, който има __next__ метод)

__next__

  • Връща следващата стойност в обхождането
  • Предизвиква специална грешка(StopIteration), когато елементите свършат
  • dunders

  • iter(a) <=> a.__iter__()
  • next(a) <=> a.__next__()
  • Има ред други подобни примери, за които споменахме предните път
  • Забележка: трябва да имате МНОГО ДОБРА основателна причина, за да ползвате a.__method__() вместо method(a)

    НЯМАТЕ такава причина

    What does the for do?

    С разликата, че for ще обработи StopIteration грешката:

    interjections = [
        'Ring-ding-ding-ding-dingeringeding!',
        'Wa-pa-pa-pa-pa-pa-pow!',
        'Hatee-hatee-hatee-ho!',
        'Joff-tchoff-tchoffo-tchoffo-tchoff!'
    ]
    iterator = iter(interjections)
    while True:
        interjection = next(iterator)
        print(interjection)
    

    Кой итерира итератора?

    Итераторите на стандартните обекти в python също имат __iter__ метод, който не прави нищо особено зашеметяващо

    >>> iterable = iter([1, 2, 3])
    >>> iter(iterable) is iterable
    True
    

    Обобщено за iter

    iter се опитва да извика __iter__ метода на аргумента си, но ако се окаже, че такъв няма конструира итератор, като просто извиква __getitem__ с последователни естествени числа, започвайки от нула, докато не се хвърли StopIteration

    class IterableThingie:
        def __getitem__(self, index):
            if index < 10:
                return index * 2
            else:
                raise StopIteration()
    
    it = IterableThingie()
    for i in it:
        print(i)
    

    Принтира 0, 2, 4, …, 18

    Обобщено за iter

    class IterableThingie:
        def __getitem__(self, index):
            if index < 10:
                return index * 2
            else:
                raise StopIteration()
    
        def __iter__(self):
            return iter('ⰰⰱⰲⰳⰴⰵⰶⰷⰸⰹⰺⰻ')
    
    it = IterableThingie()
    for i in it:
        print(i)
    

    Принтира ⰰ, ⰱ, ⰲ, ⰳ, ⰴ, ⰵ, ⰶ, ⰷ, ⰸ, ⰹ, ⰺ, ⰻ

    Стражари и апаши

    Има втора форма на iter, в която приема два аргумента, един callable обект и обект-страж.

    В този случай върнатият итератор ще връща стойностите от последователните извиквания на callable обекта, докато той не върне обекта-страж. (по-скоро обект, който при сравнение със стража връща True)

    counter = 0
    def clbl():
        # WRITING CODE LIKE THIS WILL LIKELY RESULT
        # IN THE AGONIZING UNTIMELY DEATHS OF MANY
        # CUTE FURRY ANIMALS
    
        global counter
        counter += 1
        return counter
    
    iter(clbl, 23)
    list(_)
    
    # [1, 2, 3, …, 22]
    

    Итераторите са мързеливи

    Казахме, че map, filter и range са мързеливи. Това означава, че всеки елемент се генерира чак когато е необходим.

    >>> odd = filter(lambda num: num % 2, range(10))
    >>> iter(odd) is odd
    True
    

    След като вече знаем това

    Какво ще се случи тук?

    >>> loud_names = ['JEFF', 'STONE', 'MIKE', 'EDDIE', 'MATT']
    >>> quiet_names = map(lambda name: name.lower(), loud_names)
    >>> loud_names[3] = 'VEDDER'
    >>> print(list(quiet_names))
    
    ['jeff', 'stone', 'mike', 'vedder', 'matt']
    

    sort

    list обектите имат метод sort

    >>> numbers = [12, 15, 14, 10, 5, 7, 6]
    >>> print(numbers.sort())
    None
    >>> print(numbers)
    [5, 6, 7, 10, 12, 14, 15]
    

    mutable нещата често пъти внасят усложнения

    sorted

    Има вградена функция sorted

    >>> numbers = [12, 15, 14, 10, 5, 7, 6]
    >>> print(sorted(numbers))
    [5, 6, 7, 10, 12, 14, 15]
    >>> print(numbers)
    [12, 15, 14, 10, 5, 7, 6]
    

    Сортиране по ключ

    sorted приема keyword аргумент key, който оказва как да се извлекат сравними стойности от елементите.

    >>> points = [(10, 3), (4, 8), (5, 9), (2, 3), (12, 6), (7, 4)]
    >>> sorted(points)
    [(2, 3), (4, 8), (5, 9), (7, 4), (10, 3), (12, 6)]
    >>> sorted(points, key=lambda point: point[1])
    [(10, 3), (2, 3), (7, 4), (12, 6), (4, 8), (5, 9)]
    

    reverse/reversed

    Аналогично

    >>> numbers = [12, 15, 14, 10, 5, 7, 6]
    >>> reversed(numbers)
    <list_reverseiterator object at 0x7f14ff534490>
    >>> list(_)
    [6, 7, 5, 10, 14, 15, 12]
    

    Ако добавяте/махате елементи, докато итерирате резултата от reversed, няма да останете доволни

    yield

    Генератори

    def actors_generator():
        yield 'Graham Chapman'
        yield 'John Cleese'
        yield 'Terry Gilliam'
        yield 'Eric Idle'
        yield 'Terry Jones'
        yield 'Michael Palin'
    
    actors = actors_generator()
    for actor in actors:
        print(actor + ' as seen on British TV')
    

    Защо?

    Iterator pattern

    class SquaresUpTo:
        def __init__(self, up_to):
            self.up_to = up_to
            self.num = 0
    
        def __iter__(self):
            return self
    
        def __next__(self):
            if self.num > self.up_to:
                raise StopIteration
    
            square = self.num ** 2
            self.num += 1
            return square
    

    Защо?

    Можем да го използваме ето така

    squares = SquaresUpTo(100)
    
    for square in squares:
        print(square)
    

    С генератор

    def squares_up_to(number):
        value = 0
        while value <= number:
            yield value ** 2
            value += 1
        raise StopIteration
    

    Генератори

    Generator expression

    Като list comprehension, но с обли скоби и "мързелив":

    squares_up_to_ten = (number ** 2 for number in range(10))
    

    Функции по темата

    Проверки

    Ако искаме да проверим дали елементите на итеруемо отговарят на условие

    >>> all([True, True])
    True
    >>> all([True, False])
    False
    >>> any([True, False])
    True
    

    Какво правят map и filter

    Приемат итеруеми и връщат итератори.(2 vs. 3)

    Ето добър пример защо това е хубава идея:

    def numbers():
        num = 0
        while True:
            yield num
            num += 1
    
    doulbes = map(lambda num: num*2, numbers())
    

    enumerate

    Когато индексите ни интересуват

    >>> exclamations = ['кòли', 'бèси', 'сèчи']
    >>> for index, exclamation in enumerate(exclamations):
    ...     print('{0}. {1}!'.format(index, exclamation))
    ...
    
    0. кòли!
    1. бèси!
    2. сèчи!
    

    zip

    Итерира едновременно няколко итеруеми

    titles = ['Ænima', 'Lateralus', '10,000 Days']
    positions_US = [2, 1, 1]
    positions_UK = [108, 16, 4]
    template = 'Tool\'s {0} was at {1} in the US and at {2} in the UK'
    
    for title, us_pos, uk_pos in zip(titles, positions_US, positions_UK):
        print(template.format(title, us_pos, uk_pos))
    

    itertools

    Удобства за работа с итеруеми обекти.

    Всички функции в него са „мързеливи“.

    itertools.accumulate

    >>> from itertools import accumulate
    >>> sums = accumulate(range(1, 101), lambda a, b: a + b)
    >>> print(sums)
    <itertools.accumulate object at 0x7ff61d24b518>
    >>> next(sums)
    1
    >>> next(sums)
    3
    >>> next(sums)
    6
    >>> list(sums)[-1]
    5050
    

    itertools.chain

    Конкатенира итеруеми

    >>> from itertools import chain
    >>> all_to_15 = chain(range(10), range(11, 15))
    >>> list(all_to_15)
    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14]
    

    itertools.compress

    Връща част от итеруемо според маска

    >>> from itertools import compress
    >>> list(compress(range(10), [True, False]*5)
    [0, 2, 4, 6, 8]
    

    itertools.groupby

    Групира сортирана последователност от елементи по ключ

    >>> from itertools import groupby
    >>> from collections import defaultdict
    >>> data = [ ('John', 'Tilsit'), ('Eric', 'Cheshire'), ('Michael', 'Camembert'),
    ...                  ('Terry', 'Gouda'), ('Terry', 'Port Salut'), ('Michael', 'Edam'),
    ...                  ('Eric', 'Ilchester'), ('John', 'Fynbo') ]
    >>> data = sorted(data, key=lambda record: record[0])
    >>> by_owner = defaultdict(list)
    >>> for key, group in groupby(data, lambda record: record[0]):
    ...     for record in group:
    ...         by_owner[key].append(record[1])
    ...
    >>> by_owner['Terry']
    ['Gouda', 'Port Salut']
    

    also starring

    itertools

    EXPLORE!

    import itertools
    dir(itertools)
    help(itertools)
    

    Въпроси?