- Введение
- Теория в теории
- Списковые включения или генератор списка
- Анонимные функции или lambda
- Использование lambda
- Замыкания
- Заключение
Введение
Существует несколько парадигм в программировании, например, ООП, функциональная, императивная, логическая, да много их. Мы будем говорить про функциональное программирование.
Предпосылками для полноценного функционального программирования в Python являются: функции высших порядков, развитые средства обработки списков, рекурсия, возможность организации ленивых вычислений.
Сегодня познакомимся с простыми элементами, а сложные конструкции будут в других уроках.
Теория в теории
Как и в разговоре об ООП, так и о функциональном программировании, мы стараемся избегать определений. Все-таки четкое определение дать тяжело, поэтому здесь четкого определения не будет. Однако! Хотелки для функционального языка выделим:
- Функции высшего порядка
- Чистые функции
- Неизменяемые данные
Это не полный список, но даже этого хватает чтобы сделать "красиво". Если читателю хочется больше, то вот расширенный список:
- Функции высшего порядка
- Чистые функции
- Неизменяемые данные
- Замыкания
- Ленивость
- Хвостовая рекурсия
- Алгебраические типы данных
- Pattern matching
Постепенно рассмотрим все эти моменты и как использовать в Python.
А сегодня кратко, что есть что в первом списке.
Чистые функции
Чистые функции не производят никаких наблюдаемых побочных эффектов, только возвращают результат. Не меняют глобальных переменных, ничего никуда не посылают и не печатают, не трогают объектов, и так далее. Принимают данные, что-то вычисляют, учитывая только аргументы, и возвращают новые данные.
Плюсы:
- Легче читать и понимать код
- Легче тестировать (не надо создавать «условий»)
- Надежнее, потому что не зависят от «погоды» и состояния окружения, только от аргументов
- Можно запускать параллельно, можно кешировать результат
Неизменяемые данные
Неизменяемые (иммутабельные) структуры данных - это коллекции, которые нельзя изменить. Примерно как числа. Число просто есть, его нельзя поменять. Также и неизменяемый массив — он такой, каким его создали, и всегда таким будет. Если нужно добавить элемент — придется создать новый массив.
Преимущества неизменяемых структур:
- Безопасно разделять ссылку между потоками
- Легко тестировать
- Легко отследить жизненный цикл (соответствует data flow)
theory-source
Функции высшего порядка
Функцию, принимающую другую функцию в качестве аргумента и/или возвращающую другую функцию, называют функцией высшего порядка:
def f(x):
return x + 3
def g(function, x):
return function(x) * function(x)
print(g(f, 7))
Рассмотрели теорию, начнем переходить к практике, от простого к сложному.
Списковые включения или генератор списка
Рассмотрим одну конструкцию языка, которая поможет сократить количество строк кода. Не редко уровень программиста на Python можно определить с помощью этой конструкции.
Пример кода:
for x in xrange(5, 10):
if x % 2 == 0:
x =* 2
else:
x += 1
Цикл с условием, подобные встречаются не редко. А теперь попробуем эти 5 строк превратить в одну:
>>> [x * 2 if x % 2 == 0 else x + 1 for x in xrange(5, 10)]
[6, 12, 8, 16, 10]
Недурно, 5 строк или 1. Причем выразительность повысилась и такой код проще понимать - один комментарий можно на всякий случай добавить.
В общем виде эта конструкция такова:
[stmt for var in iterable if predicate]
Стоит понимать, что если код совсем не читаем, то лучше отказаться от такой конструкции.
Анонимные функции или lambda
Продолжаем сокращать количества кода.
Функция:
def calc(x, y):
return x**2 + y**2
Функция короткая, а как минимум 2 строки потратили. Можно ли сократить такие маленькие функции? А может не оформлять в виде функций? Ведь, не всегда хочется плодить лишние функции в модуле. А если функция занимает одну строчку, то и подавно. Поэтому в языках программирования встречаются анонимные функции, которые не имеют названия.
Анонимные функции в Python реализуются с помощью лямбда-исчисления и выглядят как лямбда-выражения:
>>> lambda x, y: x**2 + y**2
<function <lambda> at 0x7fb6e34ce5f0>
Для программиста это такие же функции и с ними можно также работать.
Чтобы обращаться к анонимным функциям несколько раз, присваиваем переменной и пользуемся на здоровье.
Пример:
>>> (lambda x, y: x**2 + y**2)(1, 4)
17
>>>
>>> func = lambda x, y: x**2 + y**2
>>> func(1, 4)
17
Лямбда-функции могут выступать в качестве аргумента. Даже для других лямбд:
multiplier = lambda n: lambda k: n*k
Использование lambda
Функции без названия научились создавать, а где использовать сейчас узнаем. Стандартная библиотека предоставляет несколько функций, которые могут принимать в качестве аргумента функцию - map(), filter(), reduce(), apply().
map()
Функция map() обрабатывает одну или несколько последовательностей с помощью заданной функции.
>>> list1 = [7, 2, 3, 10, 12]
>>> list2 = [-1, 1, -5, 4, 6]
>>> list(map(lambda x, y: x*y, list1, list2))
[-7, 2, -15, 40, 72]
Мы уже познакомились с генератором списков, давайте им воспользуемся, если длина списков одинаковая:
>>> [x*y for x, y in zip(list1, list2)]
[-7, 2, -15, 40, 72]
Итак, заметно, что использование списковых включений короче, но лямбды более гибкие. Пойдем дальше.
filter()
Функция filter() позволяет фильтровать значения последовательности. В результирующем списке только те значения, для которых значение функции для элемента истинно:
>>> numbers = [10, 4, 2, -1, 6]
>>> list(filter(lambda x: x < 5, numbers)) # В результат попадают только те элементы x, для которых x < 5 истинно
[4, 2, -1]
То же самое с помощью списковых выражений:
>>> numbers = [10, 4, 2, -1, 6]
>>> [x for x in numbers if x < 5]
[4, 2, -1]
reduce()
Для организации цепочечных вычислений в списке можно использовать функцию reduce(). Например, произведение элементов списка может быть вычислено так (Python 2):
>>> numbers = [2, 3, 4, 5, 6]
>>> reduce(lambda res, x: res*x, numbers, 1)
720
Вычисления происходят в следующем порядке:
((((1*2)*3)*4)*5)*6
Цепочка вызовов связывается с помощью промежуточного результата (res). Если список пустой, просто используется третий параметр (в случае произведения нуля множителей это 1):
>>> reduce(lambda res, x: res*x, [], 1)
1
Разумеется, промежуточный результат необязательно число. Это может быть любой другой тип данных, в том числе и список. Следующий пример показывает реверс списка:
>>> reduce(lambda res, x: [x]+res, [1, 2, 3, 4], [])
[4, 3, 2, 1]
Для наиболее распространенных операций в Python есть встроенные функции:
>>> numbers = [1, 2, 3, 4, 5]
>>> sum(numbers)
15
>>> list(reversed(numbers))
[5, 4, 3, 2, 1]
В Python 3 встроенной функции reduce() нет, но её можно найти в модуле functools.
apply()
Функция для применения другой функции к позиционным и именованным аргументам, заданным списком и словарем соответственно (Python 2):
>>> def f(x, y, z, a=None, b=None):
... print x, y, z, a, b
...
>>> apply(f, [1, 2, 3], {'a': 4, 'b': 5})
1 2 3 4 5
В Python 3 вместо функции apply() следует использовать специальный синтаксис:
>>> def f(x, y, z, a=None, b=None):
... print(x, y, z, a, b)
...
>>> f(*[1, 2, 3], **{'a': 4, 'b': 5})
1 2 3 4 5
На этой встроенной функции закончим обзор стандартной библиотеки и перейдем к последнему на сегодня функциональному подходу.
Замыкания
Функции, определяемые внутри других функций, представляют собой замыкания. Зачем это нужно? Рассмотрим пример, который объяснит:
Код (вымышленный):
def processing(element, type_filter, all_data_size):
filters = Filter(all_data_size, type_filter).get_all()
for filt in filters:
element = filt.filter(element)
def main():
data = DataStorage().get_all_data()
for x in data:
processing(x, 'all', len(data))
Что можно в коде заметить: в этом коде переменные, которые живут по сути постоянно (т.е. одинаковые), но при этом мы загружаем или инициализируем по несколько раз. В итоге приходит понимание, что инициализация переменной занимает львиную долю времени в этом процессе, бывает что даже загрузка переменных в scope уменьшает производительность. Чтобы уменьшить накладные расходы необходимо использовать замыкания.
В замыкании однажды инициализируются переменные, которые затем без накладных расходов можно использовать.
Научимся оформлять замыкания:
def multiplier(n):
"multiplier(n) возвращает функцию, умножающую на n"
def mul(k):
return n*k
return mul
# того же эффекта можно добиться выражением
# multiplier = lambda n: lambda k: n*k
mul2 = multiplier(2) # mul2 - функция, умножающая на 2, например,
mul2(5) == 10
Заключение
В уроке мы рассмотрели базовые понятия ФП, а также составили список механизмов, которые будут рассмотрены в следующих уроках. Поговорили о способах уменьшения количества кода, таких как cписковые включения (генератор списка), lamda функции и их использовании и на последок было несколько слов про замыкания и для чего они нужны.
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
- Статья «Как учиться и справляться с негативными мыслями»
- Статья «Ловушки обучения»
- Статья «Сложные простые задачи по программированию»
- Вебинар «Как самостоятельно учиться»