Итераторы, генераторы и функциональное программирование

1. Итераторы — потоки данных

Итератор — объект в Python, который является объектным представлением потока данных. Говорят, что объект реализует протокол итератора, если объект:

  • имеет метод __iter__(), который возвращает объект с методом __next__(),
  • __next__() вызывает исключение StopIteration, когда поток данных закончился.

О таком объекте говоря, что он итерируемый (iterable). Итерируемый объект возвращает через __iter__() итератор (iterator).

Например, список итерируемый. У него можно найти __iter__().

In [1]:
# Список итерируемый
import inspect
inspect.getmembers([1,2,3])
Out[1]:
[('__add__', <method-wrapper '__add__' of list object at 0x10eb764c0>),
 ('__class__', list),
 ('__contains__',
  <method-wrapper '__contains__' of list object at 0x10eb764c0>),
 ('__delattr__', <method-wrapper '__delattr__' of list object at 0x10eb764c0>),
 ('__delitem__', <method-wrapper '__delitem__' of list object at 0x10eb764c0>),
 ('__dir__', <function list.__dir__()>),
 ('__doc__',
  'Built-in mutable sequence.\n\nIf no argument is given, the constructor creates a new empty list.\nThe argument must be an iterable if specified.'),
 ('__eq__', <method-wrapper '__eq__' of list object at 0x10eb764c0>),
 ('__format__', <function list.__format__(format_spec, /)>),
 ('__ge__', <method-wrapper '__ge__' of list object at 0x10eb764c0>),
 ('__getattribute__',
  <method-wrapper '__getattribute__' of list object at 0x10eb764c0>),
 ('__getitem__', <function list.__getitem__>),
 ('__gt__', <method-wrapper '__gt__' of list object at 0x10eb764c0>),
 ('__hash__', None),
 ('__iadd__', <method-wrapper '__iadd__' of list object at 0x10eb764c0>),
 ('__imul__', <method-wrapper '__imul__' of list object at 0x10eb764c0>),
 ('__init__', <method-wrapper '__init__' of list object at 0x10eb764c0>),
 ('__init_subclass__', <function list.__init_subclass__>),
 ('__iter__', <method-wrapper '__iter__' of list object at 0x10eb764c0>),
 ('__le__', <method-wrapper '__le__' of list object at 0x10eb764c0>),
 ('__len__', <method-wrapper '__len__' of list object at 0x10eb764c0>),
 ('__lt__', <method-wrapper '__lt__' of list object at 0x10eb764c0>),
 ('__mul__', <method-wrapper '__mul__' of list object at 0x10eb764c0>),
 ('__ne__', <method-wrapper '__ne__' of list object at 0x10eb764c0>),
 ('__new__', <function list.__new__(*args, **kwargs)>),
 ('__reduce__', <function list.__reduce__()>),
 ('__reduce_ex__', <function list.__reduce_ex__(protocol, /)>),
 ('__repr__', <method-wrapper '__repr__' of list object at 0x10eb764c0>),
 ('__reversed__', <function list.__reversed__()>),
 ('__rmul__', <method-wrapper '__rmul__' of list object at 0x10eb764c0>),
 ('__setattr__', <method-wrapper '__setattr__' of list object at 0x10eb764c0>),
 ('__setitem__', <method-wrapper '__setitem__' of list object at 0x10eb764c0>),
 ('__sizeof__', <function list.__sizeof__()>),
 ('__str__', <method-wrapper '__str__' of list object at 0x10eb764c0>),
 ('__subclasshook__', <function list.__subclasshook__>),
 ('append', <function list.append(object, /)>),
 ('clear', <function list.clear()>),
 ('copy', <function list.copy()>),
 ('count', <function list.count(value, /)>),
 ('extend', <function list.extend(iterable, /)>),
 ('index', <function list.index(value, start=0, stop=9223372036854775807, /)>),
 ('insert', <function list.insert(index, object, /)>),
 ('pop', <function list.pop(index=-1, /)>),
 ('remove', <function list.remove(value, /)>),
 ('reverse', <function list.reverse()>),
 ('sort', <function list.sort(*, key=None, reverse=False)>)]

Для работы с итераторами в Python есть две вспомогательные функции: iter и next. Первая возвращает итератор, вторая — следующее значение итератора.

In [2]:
# Вспомогательные функции для работы с итераторами
lst = [1,2,3]
itr = iter(lst)
next(itr), next(itr), next(itr)
Out[2]:
(1, 2, 3)
In [3]:
# Если поток закончился, то next вызовет исключение (должн быть ошибка)
next(itr)
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-3-1fa880664e34> in <module>
      1 # Если поток закончился, то next вызовет исключение (должн быть ошибка)
----> 2 next(itr)

StopIteration: 

Пример своего итерируемого объекта

Опишем объект, который возвращает целые числа от value до «бесконечности» с шагом step. Здесь сам объект имеет метод __next__(), поэтому в __iter__() он возвращает сам себя. При этом объект и итерируемый и итератор одновременно.

In [4]:
# Пример итерируемого объекта
class Counts:
    def __init__(self, start=0, step=1):
        self.value = start
        self.step = step
    def __iter__(self):
        return self
    def __next__(self):
        self.value += self.step
        return self.value
    
s = Counts(step=2)
next(s), next(s), next(s)
Out[4]:
(2, 4, 6)

NB! Итератор Counts уже реализован в модуле itertools.

2. Генераторы — упрощенные итераторы

Генератор — «синтаксический сахар» для итератора. С точки зрения синтаксиса, генератор — функция с ключевым словом yield внутри.

Чтобы сгенерировать значение, надо в next передать генератор. Внутри происходит следующее:

  1. функция-генератор выполняется до первой инструкции yield val,
  2. вычисление функции ставится на паузу,
  3. возвращается значение val.

Для примера перепишем итератор Counts как генератор.

In [5]:
def counts(value=0, step=1):
    while 1:
        value += step
        yield value
    
g = counts(step=2)
type(g), next(g), next(g), next(g)
Out[5]:
(generator, 2, 4, 6)

3. Включения в список и выражения-генераторы

  • Включения в список (англ. list comprehensions) — «синтаксический сахар» для создания списков.
  • Выражения-генераторы (англ. generator expressions) — «синтаксический сахар» для создания генераторов.

Синтаксически конструкции похожи.

In [6]:
# Включение в список
lstcomp = [x*x for x in range(10)]
type(lstcomp), lstcomp
Out[6]:
(list, [0, 1, 4, 9, 16, 25, 36, 49, 64, 81])
In [7]:
# Выражение-генератор
genexpr = (x*x for x in range(10))
type(genexpr), next(genexpr), next(genexpr), next(genexpr)
Out[7]:
(generator, 0, 1, 4)

Генератор — это процедура получения значений последовательности данных. Поэтому размер генератора меньше, чем непосредственно списка значений последовательности данных.

In [8]:
# Размер списка vs размер генератора
from sys import getsizeof

data_a = [i for i in range(1000000)]
data_b = (i for i in range(1000000))

getsizeof(data_a), getsizeof(data_b)
Out[8]:
(8697456, 112)

Генерация списка с помощью генераторов работает медленнее из-за затрат на вызов функции:

In [9]:
%timeit [i for i in range(1000000)]
118 ms ± 28.1 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [10]:
%timeit list(i for i in range(1000000))
170 ms ± 68.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

4. Функциональное программирование

Python — мультипарадигменный язык программирования. На нем можно писать в

  • процедурном,
  • объектно-ориентированном
  • или функциональном стиле.

Paradigms

Программа в функциональном стиле выглядит как последовательное применение функций к входным данным. Результат каждой их этих функций зависит только от её аргументов. Такие функции называют чистыми функциями. У нечистых функции есть побочные эффекты).

Обычно, если язык программирования поддерживает функциональный стиль, то в нём реализованы:

  • анонимные функции (лямбда-функции)
  • функции высших порядков.

5. Лямбда-функция — функции одного выражения

Лямбда-функция определеяются с помощью ключевого слова lambda. Объявление лямбда-функции аналогично объявлению функции с помощью def, только

  • в одну строчку,
  • без имени,
  • с неявным return.
In [11]:
# Лябда-функция
type(lambda x: x*x)
Out[11]:
function
In [12]:
# Применение лябда-функции
(lambda x, y: x+y)(2,3)
Out[12]:
5
In [13]:
# Применение лябда-функции как обычной функции
f=lambda x, y: x+y
f(2,3)
Out[13]:
5

Где применять лямбда-функции?

Например:

  • для компактного описания замыканий,
  • для конфигурирования функций высшего порядка (см. следующий раздел).
In [14]:
# Лябда-функция в замыканиях
def gen_mul(a):
    return lambda b: a*b

double=gen_mul(2)
double(3)
Out[14]:
6

NB! Если беглым взглядом непонятно что делает лямбда-функция, то лучше ее реализовтаь как обычную функцию.

6. Функции высшего порядка

Map, Reduce и Filter

Функции map и filter встроены в язык, а reduce находится в модуле functools.

Map, Filter, Reduce

Функция map выполняет отображение одной последовательности в другую, применяя к каждом элементу одну и ту же фукнцию.

Условно map работает так:

[a,b,c] -> [f(a),f(b),f(c)]
In [15]:
# Пример: взять для каждого элемента квадрат
map(lambda x: x*x, [1,2,3])
Out[15]:
<map at 0x10ecbc730>

map возвращает итератор. Удобно преобразовать к списку:

In [16]:
# Пример: взять для каждого элемента квадрат
list(map(lambda x: x*x, [1,2,3]))
Out[16]:
[1, 4, 9]

Функция filter выполняет фильтрацию входной последовательности, оставляя только те элементы, которые удовлетворяют критерию.

Условно filter работает так:

[a,b,c] -> {x != b} -> [a,c]
In [17]:
# Пример: оставить элементы только больше 2
list(filter(lambda x: x>2, [1,2,3]))
Out[17]:
[3]

Функция reduce выполняет сворачивание входной последовательности к одному значению, применяя последовательно функцию.

Условно reduce работает так:

[a,b,c] -> f[f[a,b],c]
In [18]:
# Пример 1: сумма элементов последовательности
from functools import reduce
reduce(lambda x,y: x+y, [1,2,3], 0)
Out[18]:
6
In [19]:
# Пример 2: скалярное произведение
from functools import reduce
a = [1,2,3]
b = [6,0,4]
reduce(lambda x,y: x+y[0]*y[1], zip(a,b), 0)
Out[19]:
18

Здесь zip — вспомогательная функция высшего порядка, которая собирает из двух последовательностей одну по такому правилу:

zip([a1,a2,a3],[b1,b2,b3]) -> [(a1,b1), (a2,b2), (a3,b3)]
In [20]:
%load_ext watermark
%watermark -d -u -v -iv
last updated: 2019-10-21 

CPython 3.8.0
IPython 7.8.0