Сообщить об ошибке.

Встроенные функции Python, используемые с итераторами

В разделе дается обзор встроенных функций и модулей Python используемых в функциональном программировании.

Содержание:

Встроенные функции.

Две встроенные функции Python, map() и filter(), дублируют функциональность выражения-генератора:

Встроенной функцией map(f, iterA, iterB, ...) можно заменить цикл for/in, так как она применяет функцию f() ко всем переданным итераторам (по сути, в цикле, тоже выполняются какие-то действия с каждым элементом), и следовательно возвращает итератор значений f(iterA[0], iterB[0]), f(iterA[1], iterB[1]), f(iterA[2], iterB[2]), ...

>>> def upper(s):
...     return s.upper()
>>> list(map(upper, ['sentence', 'fragment']))
# ['SENTENCE', 'FRAGMENT']

Конечно, можно добиться того же эффекта, используя выражение-список или как его еще называют: генератор-списков.

>>> [upper(s) for s in ['sentence', 'fragment']]
# ['SENTENCE', 'FRAGMENT']

Встроенная функция filter(predicate, iter) возвращает итератор по всем элементам последовательности, которые удовлетворяют определенному условию, и аналогичным образом дублируется при составлении списков. Аргумент predicate - это функция, которая принимает одно значение итерации iter, что-то с ним делать, и в итоге должна возвращать значение bool. Возвращаемое значение True будет говорить функции filter() пропустить значение, False - отбросить.

>>> def is_even(x):
...     return (x % 2) == 0
>>> list(filter(is_even, range(10)))
# [0, 2, 4, 6, 8]

Его также можно записать в виде выражения-списка:

>>> list(x for x in range(10) if is_even(x))
# [0, 2, 4, 6, 8]

Встроенная функция enumerate(iter, start=0) подсчитывает элементы в итерации, возвращая двойные кортежи, содержащие счётчик, начинающийся с аргумента start и элемент последовательности.

>>> for item in enumerate(['subject', 'verb', 'object']):
...     print(item)
# (0, 'subject')
# (1, 'verb')
# (2, 'object')

Функция enumerate() часто используется при просмотре/выводе индексов списка, если с элементом списка выполняется определенные условия:

with open('data.txt', 'r') as file:
    for indx, line in enumerate(file):
        if line.strip() == '':
            print(f'Строка #{indx} не содержит информации')

Встроенная функция sorted(iterable, key=None, reverse=False) собирает все элементы итерации в список, сортирует список и возвращает отсортированный результат. Аргументы key и reverse передаются методу list.sort() построенного списка.

>>> import random
# генерируем 8 случайных чисел в диапазоне [0, 10000)
>>> rand_list = random.sample(range(10000), 8)
>>> rand_list  
# [769, 7953, 9828, 6431, 8442, 9878, 6213, 2207]
>>> sorted(rand_list)  
# [769, 2207, 6213, 6431, 7953, 8442, 9828, 9878]
>>> sorted(rand_list, reverse=True)  
# [9878, 9828, 8442, 7953, 6431, 6213, 2207, 769]

Встроенные функции any(iter) и all(iter) проверяют истинные значения содержимого итерируемого объекта. Функция any() возвращает True, если какой-либо (любой) элемент в итерации является истинным значением, а all() возвращает True, если все элементы являются истинными значениями:

>>> any([0, 1, 0])
# True
>>> any([0, 0, 0])
# False
>>> any([1, 1, 1])
# True
>>> all([0, 1, 0])
# False
>>> all([0, 0, 0])
# False
>>> all([1, 1, 1])
# True

Встроенная функция zip(iterA, iterB, ...) принимает по одному элементу из каждой итерации и возвращает их в виде кортежа:

>>> list(zip(['a', 'b', 'c'], (1, 2, 3)))
# [('a', 1), ('b', 2), ('c', 3)]

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

Такое поведение очень полезно, когда надо пройтись по нескольким спискам одновременно и что то сделать с их элементами, без создания вложенных циклов:

>>> lst1 = ['a', 'b', 'c']
>>> lst2 = [1, 2, 3]
>>> for l1, l2 in zip(lst1, lst2):
...     print(f'{l1}{l2}')
# a1
# b2
# c3

Функция предназначена для использования с итерациями одинаковой длины. Если передаваемые итерируемые последовательности, в качестве аргументов, содержат разную длину, то результирующий поток будет той же длины, что и самый короткая последовательность.

>>> list(zip(['a', 'b'], (1, 2, 3)))
# [('a', 1), ('b', 2)]

Это означает, что есть риск пропустить отброшенный элемент.

Часто используемые итераторы модуля itertools.

Модуль itertools содержит ряд часто используемых итераторов, а также функции для объединения нескольких итераторов.

Функции модуля делятся на несколько широких классов:

  • Функции, которые создают новый итератор на основе существующего итератора.
  • Функции для обработки элементов итератора как аргументов функции.
  • Функции для выбора частей вывода итератора.
  • Функция для группировки вывода итератора.

Создание новых итераторов.

Функция itertools.count(start, step) возвращает бесконечный поток равномерно распределенных значений. При желании, можно указать начальное значение start, которое по умолчанию равно 0, и интервал между числами step, который по умолчанию равен 1:

>>> import itertools
>>> itertools.count()
# 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...
>>> itertools.count(10)
# 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, ...
>>> itertools.count(10, 5)
# 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, ...

Функция itertools.cycle(iter) сохраняет копию содержимого предоставленной итерации и возвращает новый итератор, который возвращает свои элементы от первого до последнего. Новый итератор будет бесконечно повторять эти элементы.

>>> import itertools
>>> itertools.cycle([1, 2, 3, 4, 5])
# 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, ...

Функция [itertools.repeat(elem, [n]) возвращает указанный элементnраз или возвращает элемент бесконечно, еслиn` не указан.

>>> import itertools
>>> itertools.repeat('abc')
# abc, abc, abc, abc, abc, abc, abc, abc, abc, abc, ...
>>> itertools.repeat('abc', 5)
# abc, abc, abc, abc, abc

Функция [`itertools.chain(iterA, iterB, ...) принимает произвольное количество итераторов в качестве входных данных и возвращает все элементы первого итератора, затем все элементы второго и так далее, пока не будут исчерпаны все итерации.

>>> import itertools
>>> itertools.chain(['a', 'b', 'c'], (1, 2, 3))
# a, b, c, 1, 2, 3

Функция [itertools.islice(iter, [start], stop, [step]) возвращает поток, являющийся срезом итератора. В отличие от [срезов строк и списков][sequence.slice.step] Python, вitertools.isliceнельзя использовать отрицательные значения дляstart,stopилиstep`.

>>> import itertools
>>> itertools.islice(range(10), 8) =>
# 0, 1, 2, 3, 4, 5, 6, 7
>>> itertools.islice(range(10), 2, 8) =>
# 2, 3, 4, 5, 6, 7
>>> itertools.islice(range(10), 2, 8, 2) =>
# 2, 4, 6

Функция itertools.tee(iter, [n]) копирует итератор, он возвращает n независимых итераторов, которые все возвращают содержимое исходного итератора. Если значение n не указано, то по умолчанию будет 2. Репликация итераторов требует сохранения некоторого содержимого исходного итератора, следовательно эта функция может потреблять значительную память, если итератор большой.

>>> import itertools
>>> iter1, iter2 = itertools.tee(range(6))
>>> list(iter1)
# 0, 1, 2, 3, 4, 5
>>> list(iter2)
# 0, 1, 2, 3, 4, 5

Вызов функций на элементах итератора.

Модуль operator содержит набор функций, соответствующих операторам Python.

Некоторые примеры:

  • operator.add(a, b): складывает два значения,
  • operator.ne(a, b): то же, что и a != b
  • operator.attrgetter('id') возвращает вызываемый объект, который выбирает атрибут .id.

Функция itertools.starmap(func, iter) предполагает, что итерируемый объект вернёт поток кортежей, и вызывает func, используя эти кортежи в качестве аргументов:

>>> import os.path
path = itertools.starmap(os.path.join,
                  [('/bin', 'python'), ('/usr', 'bin', 'java'),
                   ('/usr', 'bin', 'perl'), ('/usr', 'bin', 'ruby')])
>>> list(path)
# ['/bin/python', '/usr/bin/java', '/usr/bin/perl', '/usr/bin/ruby']

Выбор элементов итератора.

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

Функция itertools.filterfalse(predicate, iter) возвращает все элементы, для которых predicate возвращает False и является противоположностью встроенной функции filter():

>>> def is_even(x):
...     return (x % 2) == 0
>>> even = itertools.filterfalse(is_even, range(10))
>>> list(even)
# [1, 3, 5, 7, 9]

Функция itertools.takewhile(predicate, iter) возвращает элементы до тех пор, пока predicate возвращает True. Как только предикат вернет False, итератор сигнализирует об окончании своих результатов.

>>> def less_than_10(x):
...     return x < 10
...
>>> lt10 = itertools.takewhile(less_than_10, range(100))
>>> list(lt10)
# [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> itertools.takewhile(is_even, range(100))
# 0

Функция itertools.dropwhile(predicate, iter) отбрасывает элементы, пока predicate возвращает True, а затем возвращает остальные результаты итерации.

>>> test = itertools.dropwhile(less_than_10, range(20))
>>> list(test)
# [10, 11, 12, 13, 14, 15, 16, 17, 18, 19] =>

>>> test = itertools.dropwhile(is_even, range(10))
# [1, 2, 3, 4, 5, 6, 7, 8, 9]

Функция itertools.compress(data, selectors) принимает два итератора и возвращает только те элементы data, для которых соответствующий элемент selectors будет True, и останавливается, когда любой из них исчерпан:

>>> test = itertools.compress([1, 2, 3, 4, 5], [1, 1, 0, False, True])
>>> list(test)
# [1, 2, 5]

Комбинаторные функции.

Функция itertools.combinations(iterable, r) возвращает итератор, предоставляющий все возможные комбинации r-кортежей элементов, содержащихся в iterable.

>>> test = itertools.combinations([1, 2, 3, 4, 5], 2)
>>> list(test)
# (1, 2), (1, 3), (1, 4), (1, 5),
# (2, 3), (2, 4), (2, 5),
# (3, 4), (3, 5),
# (4, 5)

>>> test = itertools.combinations([1, 2, 3, 4, 5], 3)
>>> list(test)
# (1, 2, 3), (1, 2, 4), (1, 2, 5), (1, 3, 4), (1, 3, 5), (1, 4, 5),
# (2, 3, 4), (2, 3, 5), (2, 4, 5),
# (3, 4, 5)

Элементы в каждом кортеже остаются в том же порядке, в котором их вернул iterable. Например, в приведенных выше примерах цифра 1 всегда стоит перед 2, 3, 4 или 5. Аналогичная функция itertools.permutations(iterable, r=None) снимает это ограничение порядка, возвращая все возможные варианты длины r:

>>> test = itertools.permutations([1, 2, 3, 4, 5], 2)
>>> list(test)
# (1, 2), (1, 3), (1, 4), (1, 5),
# (2, 1), (2, 3), (2, 4), (2, 5),
# (3, 1), (3, 2), (3, 4), (3, 5),
# (4, 1), (4, 2), (4, 3), (4, 5),
# (5, 1), (5, 2), (5, 3), (5, 4)

Если значение для r не указано, то используется длина итерации.

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

>>> test = itertools.permutations('aba', 3) =>
>>> list(test)
# ('a', 'b', 'a'), ('a', 'a', 'b'), ('b', 'a', 'a'),
# ('b', 'a', 'a'), ('a', 'a', 'b'), ('a', 'b', 'a')

Идентичный кортеж ('a', 'a', 'b') встречается дважды, но две строки с буквой «a» пришли из разных позиций.

Функция itertools.combinations_with_replacement(iterable, r) снимает другое ограничение: элементы могут повторяться в одном кортеже. По сути, элемент выбирается для первой позиции каждого кортежа, а затем заменяется перед выбором второго элемента.

>>> test = itertools.combinations_with_replacement([1, 2, 3, 4, 5], 2)
>>> list(test)
# (1, 1), (1, 2), (1, 3), (1, 4), (1, 5),
# (2, 2), (2, 3), (2, 4), (2, 5),
# (3, 3), (3, 4), (3, 5),
# (4, 4), (4, 5),
# (5, 5)

Группировка элементов.

Последняя функция, которая обсуждается, это itertools.groupby(iter, key_func=None) и является наиболее сложной. Аргумент key_func(elem) - это функция, которая может вычислять значение ключа для каждого элемента, возвращаемого итерацией. Если не предоставить ключевую функцию, то ключом будет каждый элемент.

Функция groupby() собирает все последовательные элементы из базового итератора, которые имеют одинаковое значение ключа, и возвращает поток из двух кортежей, содержащих значение ключа и итератор для элементов с этим ключом.

city_list = [('Decatur', 'AL'), ('Huntsville', 'AL'), ('Selma', 'AL'),
             ('Anchorage', 'AK'), ('Nome', 'AK'),
             ('Flagstaff', 'AZ'), ('Phoenix', 'AZ'), ('Tucson', 'AZ'),
            ]

def get_state(el):
    return el[1]

>>> test = itertools.groupby(city_list, get_state)
>>> {abr:[el[0] for el in itr] for abr, itr in test}
# {
# 'AL': ['Decatur', 'Huntsville', 'Selma'], 
# 'AK': ['Anchorage', 'Nome'], 
# 'AZ': ['Flagstaff', 'Phoenix', 'Tucson']}
# }

Функция groupby() предполагает, что лежащее в основе итерационное содержимое уже будет отсортировано на основе ключа. Обратите внимание, что возвращаемые итераторы также используют базовый итератор, поэтому необходимо использовать результаты первого итератора, прежде чем запрашивать второй и соответствующий ему ключ.

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

Модуль functools содержит несколько функций высшего порядка, полезных в функциональном программировании. Функция высшего порядка принимает одну или несколько функций в качестве входных данных и возвращает новую функцию. Самый полезный инструмент в этом модуле - это функция functools.partial().

Для программ, написанных в функциональном стиле, иногда нужно создавать варианты существующих функций, в которых есть некоторые заполненные аргументы. Рассмотрим функцию f(a, b, c), можно создать новую функцию g(b, c), эквивалентную f(1, b, c) c одним уже заполненным аргументом a из функции f(). Это называется "частичное применение функции".

Конструктор partial() принимает аргументы (function, arg1, arg2, ..., kwarg1=value1, kwarg2=value2). Результирующий объект является вызываемым, следовательно можно просто вызвать его, чтобы вызвать function с заполненными аргументами.

Вот небольшой, но реалистичный пример:

import functools

def log(message, subsystem):
    print(f'{subsystem}: {message}')
    ...

server_log = functools.partial(log, subsystem='server')
server_log('Unable to open socket')

Следующая, часто используемая функция functools.reduce(func, iter, [initial_value]) кумулятивно выполняет операцию func для всех итерируемых элементов iter и, следовательно, не может применяться к бесконечным итерациям. Аргумент func должен быть функцией, которая принимает два элемента и возвращает одно значение. functools.reduce() берет первые два элемента A и B из итерации iter и вычисляет func(A, B). Затем она запрашивает третий элемент, C, вычисляет func(func(A, B), C), объединяет этот результат с возвращенным четвертым элементом и продолжает работу до тех пор, пока итерация не будет исчерпана. Если итерируемый объект вообще не возвращает значений, возникает исключение TypeError. Если указано начальное значение initial_value, то оно используется в качестве отправной точки, а первое вычисление будет func(initial_value, A).

>>> import operator, functools
>>> functools.reduce(operator.concat, ['A', 'BB', 'C'])
# 'ABBC'
>>> functools.reduce(operator.concat, [])
# Traceback (most recent call last):
#   ...
# TypeError: reduce() of empty sequence with no initial value
>>> functools.reduce(operator.mul, [1, 2, 3], 1)
# 6
>>> functools.reduce(operator.mul, [], 1)
# 1

Если использовать operator.add() с functools.reduce(), то можно сложить все элементы итерации. Этот случай настолько распространен, что для его вычисления есть специальная функция sum():

>>> import functools, operator
>>> functools.reduce(operator.add, [1, 2, 3, 4], 0)
# 10
>>> sum([1, 2, 3, 4])
# 10
>>> sum([])
# 0

Но для многих случаев использования functools.reduce() будет проще написать очевидный цикл for:

import functools
# например, вместо
product = functools.reduce(operator.mul, [1, 2, 3], 1)

# можно написать
product = 1
for i in [1, 2, 3]:
    product *= i

Связанная функция itertools.accumulate(iterable, func=operator.add). Она выполняет те же вычисления, но возвращает не только окончательный результат, accumulate() возвращает итератор, который также отдаёт каждый частичный результат:

itertools.accumulate([1, 2, 3, 4, 5]) =>
  1, 3, 6, 10, 15

itertools.accumulate([1, 2, 3, 4, 5], operator.mul) =>
  1, 2, 6, 24, 120

Лямбда-выражения.

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

Если есть встроенная функция Python или подходящая функция модуля, то вообще не нужно определять новую функцию:

stripped_lines = [line.strip() for line in lines]
existing_files = filter(os.path.exists, file_list)

Если подходящей функции не существует, то её нужно написать. Один из способов написания небольших функций, это использовать `lambda-выражение. Анонимная функция (lambda-выражение) принимает ряд параметров, которые объединяются в выражении:

adder = lambda x, y: x+y

print_assign = lambda name, value: name + '=' + str(value)

Альтернативой является использование оператора def и определение функции обычным способом:

def adder(x, y):
    return x + y

def print_assign(name, value):
    return name + '=' + str(value)

Анонимная функция (lambda-выражение) весьма ограничена в возможностях. Результат должен быть получен в одно выражение, следовательно нет возможности использовать операций сравнения if/elif/else или операторов try/except. Если попытаться сделать слишком много в операторе lambda, то получится слишком сложное выражение, которое трудно читать.

import functools

total = functools.reduce(lambda a, b: (0, a[1] + b[1]), items)[1]

Понять выражение, представленное выше можно, но нужно время, чтобы понять, что происходит. Использование коротких вложенных операторов def немного улучшает ситуацию:

import functools
def combine(a, b):
    return 0, a[1] + b[1]

total = functools.reduce(combine, items)[1]

Но лучше всего, просто использовать цикл for:

total = 0
for a, b in items:
    total += b

Или встроенную функцию sum() и выражение-генератор:

total = sum(b for a, b in items)

Многие варианты использования functools.reduce() более понятны, если записать их в виде циклов for/in.

Фредрик Лунд однажды предложил следующий набор правил для рефакторинга использования lambda:

  • Напишите лямбда-функцию.
  • Напишите комментарий, объясняющий, что делает лямбда-функция.
  • Изучите комментарий и придумайте имя, которое отражает суть комментария.
  • Преобразуйте лямбда-функцию в оператор def, используя это имя.
  • Удалите комментарий.