Учебная страница курса биоинформатики,
год поступления 2013
Фнукциональное программирование в Python: генераторы и лямбда-функции
или сказ о том, как написать программу на питоне в 1 строку
Важно: всем крайне рекомендуется к прочтению прекрасный сборник "трюков" на питоне, где раскрыты темы из этого занятия, а также множество других тем. Тем, кто хочет считать себя опытным программистом, читать обязательно. В данном уроке используются материалы этого сборника.
А конкретно эта тема неплохо раскрыта на википедии
Лямбда-функции
Лямбда-функции - это (упрощенно) такие функции, которые записаны в одну строку, и определены не с помощью ключевого слова def отдельно, а прямо в месте применения. Если вы используете какую-то функцию многократно, то лямбда-функция для нее вам, конечно, не нужна - лучше (читабельнее) определить эту функцию один раз с помощью def, а потом использовать ее в коде. Но если вам нужно на лету в одном конкретном месте сделать несложное превращение - лямбды очень удобны.
Чтобы разобраться, как они устроены, определим лямбда-функцию возведения в квадрат:
1 f = lambda x: x*x
это будет эквиваленто:
Обратите внимание на тонкости определения:
слово def не используется, вместо него - знак равенства и слово lambda;
параметры лямбда-функции указываются не в скобках, как вы привыкли, а после слова lambda, перед двоеточием (если переменных несколько - они разделяются запятыми, также без скобок);
возвращаемое значение указывается без слова return после двоеточия
Еще пример (следующие функции идентичны):
1 def add(a, b):
2 c = a + b
3 return c
4
5 def add_short(a, b):
6 return a + b
7
8 add_lambda = lambda a, b: a + b # обратите внимание! add_lambda - это не переменная, а имя функции
9
10 >>> 3+6
11 9
12 >>> add(3,6)
13 9
14 >>> add_short(3,6)
15 9
16 >>> add_lambda(3,6) # и вызывается со скобочками, как обычная функция, хотя в определении функции выше скобок не было
17 9
Однако сила лямбда-функций не в том, чтобы сокращать определение функций. Главное - что их можно использовать без присвоения их конкретной переменной в однострочных (inline) выражениях. Применение таких выражений мы рассмотрим в следующей части, а пока пример:
Но реальная польза от лямбд есть - это их использование в функции sorted():
1 >>> d = {1:1, 2:3, 5:2}
2 >>> d_items = d.items()
3 >>> d_items
4 [(1,1),(2,3),(5,2)]
5 # чтобы сортировать по второму элементу кортежа создадим лямбда-функцию, которая умеет брать второй элемент кортежа:
6 >>> second = lambda x: x[1] # эквивалентно def second(x): return x[1]
7 >>> second( (1,2) )
8 2
9 # и теперь сортируем с помощью параметра key функции sorted:
10 >>> sorted(d_items, key = second)
11 [(1,1),(5,2),(2,3)] # новый список сортирован по второму элементу каждого кортежа
12
13 # так можно превратить элементы словаря в список, сортированный по значениям словаря
14 # как мы уже знаем, лямбду можно не определять заранее, а подставить сразу в место использования:
15 >>> sorted(d.items(), key = lambda x: x[1])
16 [(1,1),(5,2),(2,3)]
17 # но помните, что такая лямбда не проверяет, что у x есть элемент [1] - для словаря это не страшно, т.к. все элементы d.items() - всегда кортежи; но в общем случае - это ваша задача написать такую лямбду, которая учитывает особенности данных, иначе будете падать с исключениями
18 # для этого можно использовать inline if (см. далее):
19
20 # сортировка элементов словаря по абсолютной величине значения:
21 >>> d = {1:1, 2:3, 5:-2}
22 >>> sorted(d.items(), key = lambda x: x[1] if x[1] >= 0 else -x[1])
23 [(1,1),(5,-2),(2,3)] # при этом исходные значения сохраняются
Генераторы списков
Генераторы списков - это элегантный способ уместить создание списка и фильтрацию его элементов (иначе говоря - цикл for, блок if и присваивание) в одну строку. Рассмотрим простой пример с созданием списка квадратов чисел:
* Вопрос на засыпку:
1 numbers = range(1,6), xrange(1,6) для python2, range(1,6) для python3
В чем разница между range(5) и xrange(5) в python2? С чем из них совпадает range(5) в python3?
Для превращения одного списка в другой в python есть функция map(f, iter): она берет на вход перебираемый (итерируемый) объект iter, к каждому элементу применяет функцию f, и возвращает новый итерируемый объект. Её можно использовать здесь следующим образом:
Мы уже знаем, что такую функцию square можно упростить с помощью лябмды - и вот как это делается:
И как уже говорилось выше - лямбда-функции не обязательно присваивать в переменную, и весь код можно переписать следующим образом:
А теперь все то же самое, но без лямбд, и гораздо более читабельно:
Собственно это и есть генератор списка.
* не стоит сбрасывать map со счетов только потому, что есть такие клевые генераторы; он также удобен хотя бы потому, что есть его версия map_async, которая позволяет вам легко (практически не меняя код) выполнять параллельные вычисления, но в данном уроке мы эту тему рассматривать не будем
Так же, как и один цикл for можно перебирать два и более:
Общий синтаксис генераторов:
[ result for var in iter if condition ], где:
iter - исходный перечисляемый объект (например, список или файл(!))
var - имя для переменной, в которую перебирается iter, эквивалентно for var in iter:
result - любое выражение, использующее var, которое должно быть подсчитано, а его результат - записан в новый список
condition - дополнительное условие, проверяющее что-то связанное с var - только соответствующие var будут обработаны, а остальные - отфильтрованы и выброшены.
Например:
Только помните, что if перед condition - фильтрует переменные. Если вам нужно что-то вычислять по разному для разных var, но вывести все - вы можете использовать if внутри result. Это так называемый inline if: конструкция вида "a if b else c". Она эквивалентна:
Удобство данной конструкции в том, что ее можно также, как и лямбды - вставлять в любое место кода, как цельное выражение, например:
И теперь для вычисления разных результатов для разных элементов в генераторе списков мы можем подставить этот inline if:
Генераторы словарей и сетов
Напомню, что словарь - это структура типа "ключ->значение", где ключи - уникальны; а сет - это такой "словарь без значений", где есть только одни ключи (что очень похоже на список, не запоминающий порядок элементов и содержащий только неповторяющиеся элементы). Хороши обе эти структуры тем, что поиск в них по ключу происходит мгновенно, сколько бы данных в них не хранилось, а вот поиск в списке будет идти тем дольше, чем больше у вас в списке элементов.
Так вот, точно так же, как и списки, можно генерировать словари и сеты (set):
1 >>> squares_list = [ x*x for x in range(1,6) ]
2 >>> squares_list
3 [1,4,9,16,25]
4 >>> squares_dict = { x: x*x for x in range(1,6) } # генерируем словарь так же, как и список, только указываем "ключ:значение", и используем фигурные скобки вместо квадратных
5 >>> squares_dict
6 {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
7 >>> squares_set = { x for x in range(1,6) } # тоже фигурные скобки, но без "ключ:значение", только ключи
8 >>> squares_set
9 set([1,2,3,4,5])
* Ну и конечно то же самое можно сделать через генератор списков, получив список и превратив его в словарь, только это будет в несколько (~4) раз медленнее, так что не рекомендуем:
1 >>> squares_dict = { x: x*x for x in range(1,6) } # генерируем словарь так же, как и список, только указываем "ключ:значение", и используем фигурные скобки вместо квадратных
2 >>> squares_dict
3 {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
4 >>> squares_dict = dict( [ (x, x*x) for x in range(1,6) ] )
5 >>> squares_dict
6 {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
* Ленивые вычисления на примере выражений-генераторов
Ленивыми вычислениями называется подход, при котором функции не вычисляются до тех пор, пока результат их работы не потребуется кому-то другому.
Очень упрощенным примером ленивых вычислений можно считать выражения-генераторы: итераторы, которые не хранят в памяти объекты целиком. Вспомните, что когда вы открываете файл, то вы можете перебирать его строки (for line in file), однако сам файл при этом в память целиком не грузится, а грузятся строки по одной, только тогда, когда они понадобились. Это очень хорошо, т.к. если ваш файл размером в несколько сотен гигабайт, то вы бы наверняка хотели избежать его загрузки в память целиком. Таким образом открытый файл - генератор: это итерируемый (перебираемый) объект, но при этом он не хранит в памяти все свое содержимое.
Вы тоже можете создавать такие объекты на питоне, и синтаксис его очень похож на генераторы списков:
В данном случае генератор squares_iter - как раз пример "ленивых вычислений", т.к. каждый элемент этого итератора не вычисляется до тех пор, пока до него не дойдет дело при переборе. См. задание №3 для подробностей.
Однако помните! что такие итерируемые объекты требуют расплаты за то, что вам не нужно хранить в памяти их содержимое - а именно, к каждому их элементу можно обратиться только один раз!
Если вы хотите создать генератор, который сложнее, чем 1 строка, то вам необходимо использовать ключевое слово yield, однако поскольку генераторы даже в виде однострочников уже достаточно взывают мозг, то мы не будем разбирать в этом занятии, как он работает. Но если вы будете писать серьезный софт на python, то без полноценных генераторов вам не обойтись. Любопытствующих отправляю к следующему коду, статье на хабре про генераторы и yield, и к vcf.py из 3 задания:
1 # создадим список в цикле
2 def list_squares_to_ten():
3 l = []
4 for i in xrange(10):
5 l.append(i*i)
6 return l
7 # или то же самое генератором списков
8 a = [ x*x for x in xrange(10) ]
9 # а теперь просто генератор
10 a = ( x*x for x in xrange(10) )
11 # эквивалетен следующему коду
12 def gen_squares_to_ten():
13 # l = [] # больше не нужен
14 print('just started!')
15 for i in xrange(10):
16 print('one more')
17 yield i*i # что вам нужно знать про yield:
18 # а) функции с таким ключевым словом возвращают (хотя return в них нигде нет) генератор;
19 # б) они выполняются не при их вызове в коде, а при обращении к элементам генератора;
20 # в) при обращении к первому элементу код функции выполняется от начала до первого yield, после чего выполнение функции "ставится на паузу", а в обратившемся "for val in iter" в val помещается значение, указанное после yield;
21 # г) когда происходит обращение к следущему элементу генератора, выполнение функции "размораживается" и продолжается до следующего yield, далее ставится на паузу снова, а в val кладется следующее значение; и так, пока функция не закончит работать
22 # return l # больше не нужен
23
24 >>> a = gen_squares_to_ten()
25 >>> # эй, мы ничего не забыли напечатать? а как же 'just started!' ?
26 >>> for j in a:
27 print j
28 just started!
29 one more
30 0
31 one more
32 1
33 one more
34 4
35 one more
36 9
37 one more
38 16
39 one more
40 25
41 one more
42 36
43 one more
44 49
45 one more
46 64
47 one more
48 81
49 >>> WTF?
50 SyntaxError: invalid syntax