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

lambda, ФВП, map(), reduce(), filter(), zip(), enumerate()

Введение

Функциональное программирование — это такая методика написания программ, когда в центре внимания находятся функции. Функции могут присваиваться переменным, они могут передаваться в другие функции и порождать новые функции. Это можно назвать стилем или парадигмой программирования

В этой парадигме широко используются функции высшего порядка и анонимные функции

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

Анонимные функции (lambda-функции) — безымянные функции

Анонимные функции (lambda)

Вспомним:

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

  • Чтобы вызвать функцию, нужно указать её имя, но бывают такие функции, у которых нет имени, но их всё равно можно вызывать

Пример обычной функции:

def add_func(x, y):
    result = x + y
    return result
print(add_func(2,3))
  • У функции есть имя, тело функции и есть результат, который она возвращает. Чтобы вызвать эту функцию, мы указываем имя и аргументы, которые нужно обработать.

  • Мы можем значительно усложнить функцию, для этого добавить в неё нужные инструкции

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

  • lambda аргументы: выражение (значение функции)

result = lambda x, y: x + y
print(result(2, 3))
  • лямбда-функция всегда возвращает значение

  • она должна быть записана в одну строку

  • внутри функций нет присваивания и сложной логики — это всегда простое выражение

Зачем нужны лямбда-функции?

  • Самое частое применение — в качестве аргументов в других функциях

  • Пример:

def filter_numbers(numbers, filter_func):
    result = []
    for num in numbers:
        if filter_func(num):
            result.append(num)
    return result


numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
filtered_numbers = filter_numbers(numbers, lambda x: x % 2 == 0) # передаём анонимную  функцию в качестве аргумента
print(filtered_numbers)
# [2, 4, 6, 8, 10]
  • Пойдём дальше — мы можем вместо filter_numbers тоже использовать лямбда-функцию:

numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
filtered_numbers = lambda lst: [num for num in lst if num % 2 == 0]
# здесь анонимная функция принимает lst и формирует список при помощи спискового включения
print(filtered_numbers(numbers))
# [2, 4, 6, 8, 10]
  • Технически, можно обойтись без лямбда-функций. Но иногда с ними получается удобнее

Практика:

  • Напишите четыре анонимные функции. Они должны:

    • Возводить переданное число в квадрат

    • Умножать два переданных числа

    • Возвращать бОльшее из двух переданных чисел

    • Возвращать первую букву переданного слова

  • Выведите на экран результат работы этих функций

Встроенные ФВП: map() filter() reduce()

map()

  • Допустим, мы хотим применить к каждому элементу списка какую-то функцию или операцию. Мы можем сделать при помощи цикла for:

numbers = [1, 2, 3, 4, 5]
new_numbers = []
for num in numbers:
    squared = num ** 2
    subtracted = squared - 10
    new_numbers.append(subtracted)

print(new_numbers)
# [-9, -6, -1, 6, 15]
  • Можем определить две функции, одна из которых принимает последовательность и функцию, а вторая является той функцией, которую нужно применить:

def func_1(numbers, func):
    res = []
    for num in numbers:
        res.append(func(num))
    return res
    
def func_2(number):
    number **= 2
    number -= 10
    return number
    
numbers = [1, 2, 3, 4, 5]
print(func_1(numbers, func_2))
# [-9, -6, -1, 6, 15]
  • Есть третий способ — использовать встроенную функцию высшего порядка map()

  • Она принимает в качестве аргументов функцию и последовательность. И применяет переданную функцию к каждому элементу последовательности

def func_2(number):
    number **= 2
    number -= 10
    return number

numbers = [1, 2, 3, 4, 5]
print(list(map(func_2, numbers))) # преобразуем в list, т.к. map возращает объект типа map
# [-9, -6, -1, 6, 15]
  • Часто вместе в качестве аргументов для ФВП используют анонимные функции:

numbers = [1, 2, 3, 4, 5]
print(list(map(lambda n: n ** 2 - 10, numbers)))
# [-9, -6, -1, 6, 15]

filter()

filter(function or None, iterable) -> filter object
  • Как мы видим из сигнатуры, функция принимает функцию или None и итерируемую последовательность. Возвращает объект filter

numbers = [2, 3, 8, 15, 34, 42]
def is_even(number):
    return number % 2 == 0 

filter(is_even, numbers)
list(filter(is_even, numbers))
# [2, 8, 34, 42]
  • filter() принимает функцию, которая возвращает булево значение

  • Попробуйте передать в качестве аргумента вместо функции значение None. Проанализируйте результат

reduce()

  • Для того, чтобы воспользоваться функцией reduce()нам понадобится импортировать её из модуля functools

from functools import reduce

reduce(function, iterable[, initializer]) -> value
  • Функция reduce() кумулятивно применяет функцию function к элементам итерируемой iterable последовательности, сводя её к единственному значению

  • По сути результат работы reduce() это аккумулятор

reduce(lambda x, y: x + y, [1, 2, 3, 4, 5])
# 15
items = [10, 20, 30, 40, 50]
sum_all = reduce(lambda x, y: x + y, items)
sum_all
# 150
items = [1, 24, 17, 14, 9, 32, 2]
all_max = reduce(lambda a, b: a if (a > b) else b, items)
all_max
# 32
  • Необязательный аргумент initializer — это значение аккумулятора по умолчанию, с которого начать работу. Если последовательность пустая, аккумулятор примет значение initializer. Если последовательность непустая, то initializer станет начальным значением аккумулятора

  • Попробуйте применить аргумент initializer к примерам выше. Проанализируйте результат

Особенности объектов map, filter, reduce

  • Результат работы функций map() filter() reduce() это объекты map filter reduce соответственно. Это не готовые последовательности, а итераторы — интерфейсы для взаимодействия с последовательностями, но не сами последовательности.

  • Мы можем использовать их как iterable объекты, но не можем увидеть всю последовательность.

  • Сама последовательность скрыта и данные предоставляются "по запросу" — то есть только тогда, когда это потребуется

  • Это позволяет эффективнее использовать память, чем при хранении, например, списка

  • Если нам нужна сама последовательность, результат работы map() filter() reduce() нужно преобразовать в неё

c = reduce(lambda x, y: x + y, map(lambda x: x * 2, [1, 2, 3, 4, 5]))
# будет работать, т.к. объект map является iterable
res = map(lambda x: x * 2, [1, 2, 3, 4, 5])
res
# map object
list(res)
# [2, 4, 6, 8, 10]

Практика:

Задание с числами:

  • Входные данные: список чисел.

  • Используйте map(), чтобы возвести каждое число в квадрат.

  • Затем к получившейся последовательности примените filter(), чтобы отфильтровать только нечётные числа.

  • И к получившейся в результате последней операции последовательности примените reduce(), чтобы сложить оставшиеся числа в списке.

Задание с текстом:

  • Входные данные: список строк. Также как в прошлом задании, последовательно примените функции к списку строк.

    • Используйте map(), чтобы преобразовать каждую строку в верхний регистр.

    • Используйте filter(), чтобы отфильтровать строки, которые содержат определенную подстроку

    • Используйте reduce(), чтобы объединить отфильтрованные строки в одну строку через запятую.

    • Верните результат.

  • Для тестирования используйте следующие данные:

['I love Python', 'You love Python', 'We love Python', "I don't love anything"]
Подстрока: 'PYTHON'

Некоторые полезные функции

zip()

  • Функция zip() — объединяет отдельные элементы нескольких последовательностей в кортежи. Возвращает объект zip

zip(последовательность, последовательность, ...)
  • Примеры использования:

x = 'абв'
y = 'эюя'
zipped = zip(x, y)
list(zipped)
# [('а', 'э'), ('б', 'ю'), ('в', 'я')]
names = ["Alice", "Bob", "Charlie"]
scores = [85, 92, 78]

zipped = zip(names, scores)

for item in zipped:
    print(item)
    
# ('Alice', 85)
# ('Bob', 92)
# ('Charlie', 78)
  • Попробуйте применить zip() к последовательностям разной длины. Проанализируйте результат

  • zip() часто используется для создания словарей. В главе, посвященной словарям, мы узнаем, как это работает

enumerate()

  • Функция enumerate возвращает индекс элемента и сам элемент последовательности в качестве кортежа. Вот общий формат функции enumerate:

enumerate(последовательность)
  • Пример использования:

letters = enumerate(['а','б','в'])
list(letters)
[(0, 'а'), (1, 'б'), (2, 'в')]
  • enumerate() часто используют в цикле for, когда нам нужен и элемент и его индекс:

names = ['Alice', 'Bob', 'John', 'Elena']
for i, name in enumerate(names):
    print(f'{name} имеет индекс {i}')

Практика:

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

Last updated