Генераторы и цикл for

Цикл for.

Цикл for может использоваться для различных целей.

Самый простой пример использования цикла:

for i in range(5):
    print(i)
0
1
2
3
4

При помощи этого цикла можно итерироваться по любому объекту-коллекции:

lst = ["qwerty", 12345, 34.42]

for i in lst:
    print(i)
qwerty
12345
34.42

Но в таком случае встает вопрос, что же общего между объектом-коллекцией и диапазоном значений? range является функцией. Попробуем посмотреть, что эта функция возвращает:

a = range(5)

print("object:\n\t", a)
print("type:\n\t", type(a))
print("Methods and attributes:\n\t", dir(a))
object:
     range(0, 5)
type:
     <class 'range'>
Methods and attributes:
     ['__bool__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'count', 'index', 'start', 'step', 'stop']

То есть range -- это класс и мы вызываем его конструктор. Объект этого класса является итерируемым, а значит с ним может работать цикл for. Чтобы создать итератор из объекта, воспользуемся функцией iter():

iterator = iter(a)

print("object:\n\t", iterator)
print("type:\n\t", type(iterator))
print("Methods and attributes:\n\t", dir(iterator))
object:
     <range_iterator object at 0x0000012FA12F9CF0>
type:
     <class 'range_iterator'>
Methods and attributes:
     ['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__length_hint__', '__lt__', '__ne__', '__new__', '__next__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setstate__', '__sizeof__', '__str__', '__subclasshook__']

Итератор — объект, который знает свое текущее состояние и может вычислить следующее значение. Такой подход не приводит к созданию дополнительных больших объектов в памяти и таким образом делает программу более эффективной. Никакой лишней информации при этом в памяти не хранится.

Для того, чтобы перейти к следующему состоянию, используется функция next().

print(next(iterator))
print(next(iterator))
print(next(iterator))
print(next(iterator))
print(next(iterator))
0
1
2
3
4

Но что же происходит, когда мы пытаемся получить следующий объект, но его не существует?

next(iterator)
---------------------------------------------------------------------------

StopIteration                             Traceback (most recent call last)

<ipython-input-19-4ce711c44abc> in <module>()
----> 1 next(iterator)


StopIteration:

В таком случае выпадает ошибка StopIteration, которая говорит, что следующий объект получить невозможно. Это и является признаком конца итерации. На эту ошибку и ориентируется цикл for.

Упражнение 1

Вам дана функция на языке python:

def print_map(function, iterable):
    for i in iterable:
        print(function(i))

Требуется переписать данную функцию не используя цикл for. ****

Генераторы

Рассмотрим несколько примеров итерируемых объектов, которые есть в языке python (кроме range).

map(function, iterable)

В начале рассмотрим функцию map(func, iterable). Эта функция позволяет применить некоторую другую функцию func ко всем элементам другого итерируемого объекта iterable. Обратите внимание, что объект-функция передается без круглых скобок

def baz(value):
    return value * value

lst = [1, 2, 3, 4, 5]

for i in map(baz, lst):
    print(i)
1
4
9
16
25

zip(iterable[, iterable, ...])

Функция zip(iterable[, iterable, ...]) позволяет параллельно итерироваться по большому количеству итерируемых объектов, получая из них соответствующие элементы в виде кортежа. Итератор прекращает свою работу, когда один из переданных объектов закончится.

names = ["Alex", "Bob", "Alice", "John", "Ann"]
age = [25, 17, 34, 24, 42]
sex = ["M", "M", "F", "M", "F"]

for values in zip(names, age, sex):
    print("name: {:>10} age: {:3} sex: {:2}".format(*values))
name:       Alex age:  25 sex: M
name:        Bob age:  17 sex: M
name:      Alice age:  34 sex: F
name:       John age:  24 sex: M
name:        Ann age:  42 sex: F

filter(func, iterable)

Пробегает по итерируемому объекту и возвращает только те элементы, которые удовлетворяют условию, описанному в функции func.

def bar(x):
    if abs((34-x*x))**0.5 > x:
        return True
    return False

for i in filter(bar, [0, 1, 2, 3, 4, 5]):
    print(i)
0
1
2
3
4

enumerate(iterable, start=0)

Принимает на вход итерируемый объект и возвращает пары (индекс элемента, элемент). Индексация начинается со start, который по умолчанию равен 0.

names = ["Alex", "Bob", "Alice", "John", "Ann"]

for idx, elem in enumerate(names, 1):
    print("{:02}: {:>7}".format(idx, elem))
01:    Alex
02:     Bob
03:   Alice
04:    John
05:     Ann

Кажется, что концепция генерации объектов налету, без предварительного выделения памяти под целый массив, является довольно удобной и полезной. Объекты-итераторы могут хранить, например, списки запросов к серверу, логи системы и другую информацию, которую можно обрабатывать последовательно. В таком случае, нам хочется научиться создавать подобные объекты.

Для этих целей может использоваться ключевое слово yield. Функция, в которой содержится это ключевое слово, становится функцией-генератором. Из такой функции можно создать объект-итератор. При вызове функции next() выполнение этой функции дойдет до первого встреченного ключевого слова yield, после чего, подобно действию return, управление перейдет основной программе. Поток управления вернется обратно в функцию при следующем вызове next() и продолжит выполнение с того места, на котором остановился ранее.

Рассмотрим, каким образом можно написать свою собственную функцию range():

def my_range(a, b=None, step=1):
    if b is None:
        a, b = 0, a
    _current = a
    while True:
        yield _current
        _next = _current + step
        if (_next - b)*(_current - b) <= 0:
            break
        _current = _next

for i in my_range(5):
    print(i, end = " ")
print()

for i in my_range(1, 5):
    print(i, end = " ")
print()

for i in my_range(1, 10, 2):
    print(i, end = " ")
print()

for i in my_range(10, 0, -3):
    print(i, end = " ")
print()
0 1 2 3 4
1 2 3 4
1 3 5 7 9
10 7 4 1

Упражнение 2

Напишите генератор, выводящий первые n чисел Фибоначчи. ***

Упражнение 3

Реализуйте аналог функций zip, map, enumerate. ***

Большое количество различных итерируемых объектов содержится в библиотеке itertools. Функции приведены в таблицах ниже:

Iterator Arguments Results Example
count()_ start, [step] start, start+step, start+2*step, … count(10) --> 10 11 12 13 14 ...
cycle()_ p p0, p1, … plast, p0, p1, … cycle('ABCD') --> A B C D A B C D ...
repeat()_ elem [,n] elem, elem, elem, … endlessly or up to n times repeat(10, 3) --> 10 10 10
Iterator Arguments Results Example
accumulate()_ p [,func] p0, p0+p1, p0+p1+p2, … accumulate([1,2,3,4,5]) --> 1 3 6 10 15
chain()_ p, q, … p0, p1, … plast, q0, q1, … chain('ABC', 'DEF') --> A B C D E F
chain.from_iterable()_ iterable p0, p1, … plast, q0, q1, … chain.from_iterable(['ABC', 'DEF']) --> A B C D E F
compress()_ data, selectors (d[0] if s[0]), (d[1] if s[1]), … compress('ABCDEF', [1,0,1,0,1,1]) --> A C E F
dropwhile()_ pred, seq seq[n], seq[n+1], starting when pred fails dropwhile(lambda x: x<5, [1,4,6,4,1]) --> 6 4 1
filterfalse()_ pred, seq elements of seq where pred(elem) is false filterfalse(lambda x: x%2, range(10)) --> 0 2 4 6 8
groupby()_ iterable[, key] sub-iterators grouped by value of key(v)  
islice()_ seq, [start,] stop [, step] elements from seq[start:stop:step] islice('ABCDEFG', 2, None) --> C D E F G
starmap()_ func, seq func(*seq[0]), func(*seq[1]), … starmap(pow, [(2,5), (3,2), (10,3)]) --> 32 9 1000
takewhile()_ pred, seq seq[0], seq[1], until pred fails takewhile(lambda x: x<5, [1,4,6,4,1]) --> 1 4
tee()_ it, n it1, it2, … itn splits one iterator into n  
zip_longest()_ p, q, … (p[0], q[0]), (p[1], q[1]), … zip_longest('ABCD', 'xy', fillvalue='-') --> Ax By C- D-
Iterator Arguments Results
product()_ p, q, … [repeat=1] cartesian product, equivalent to a nested for-loop
permutations()_ p[, r] r-length tuples, all possible orderings, no repeated elements
combinations()_ p, r r-length tuples, in sorted order, no repeated elements
combinations_with_replacement()_ p, r r-length tuples, in sorted order, with repeated elements
Examples Results
product('ABCD', repeat=2) AA AB AC AD BA BB BC BD CA CB CC CD DA DB DC DD
permutations('ABCD', 2) AB AC AD BA BC BD CA CB CD DA DB DC
combinations('ABCD', 2) AB AC AD BC BD CD
combinations_with_replacement('ABCD',2) AA AB AC AD BB BC BD CC CD DD

Упражнение №1

Написать функцию, принимающую 2 списка и возвращающую декартово произведение (использовать itertools.product)

def get_cartesian_product(a, b):
    raise RuntimeError("Not implemented")

get_cartesian_product([1, 2], [3, 4]) == [(1, 3), (1, 4), (2, 3), (2, 4)]

Упражнение №2

Написать функцию, принимающую строку s и число n и возвращающую всевозможные перестановки из n символов в s строке в лексикографическом(!) порядке (использовать itertools.permutations)

def get_permutations(s, n):
    raise RuntimeError("Not implemented")

get_permutations("cat", 2) == ["ac", "at", "ca", "ct", "ta", "tc"]

Упражнение №3

Реализовать функцию get_combinations. Должна принимать строку s и число k и возвращать все возможные комбинации из символов в строке s с длинами <= k (использовать itertools.combinations)

def get_combinations(s, n):
    raise RuntimeError("Not implemented")

get_combinations("cat", 2) == ["a", "c", "t", "ac", "at", "ct"]

Упражнение №4

Функция должна принимать строку s и число k и возвращать все возможные комбинации из символов в строке s с длинами = k с повторениями (использовать itertools.combinations_with_replacement)

def get_combinations_with_r(s, n):
    raise RuntimeError("Not implemented")

get_combinations_with_r("cat", 2) == ["aa", "ac", "at", "cc", "ct", "tt"]

Упражнение №5

Написать функцию, которая подсчитывает количество подряд идующих символов в строке (использовать itertools.groupby)

def compress_string(s):
    raise RuntimeError("Not implemented")

compress_string('1222311') == [(1, 1), (3, 2), (1, 3), (2, 1)]

Упражнение №6

В функцию передается список списков. Нужно вернуть максимум, который достигает выражение $(a_1^2 + a_2^2 + ... + a_n^2) % m $. Где ai --- максимальный элемент из i-ого списка (использовать функцию из itertools)

def maximize(lists, m):
    raise RuntimeError("Not implemented")

lists = [
    [5, 4],
    [7, 8, 9],
    [5, 7, 8, 9, 10]
]
maximize(lists, m=1000) == 206

В примере = 206, так как это максимум от суммы (a21 + a22 + a23)%1000

a1 = 5, a2 = 9, a3 = 10