Kodomo

Пользователь

Учебная страница курса биоинформатики,
год поступления 2013

Фнукциональное программирование в Python: генераторы и лямбда-функции

или сказ о том, как написать программу на питоне в 1 строку

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

А конкретно эта тема неплохо раскрыта на википедии

Лямбда-функции

Лямбда-функции - это (упрощенно) такие функции, которые записаны в одну строку, и определены не с помощью ключевого слова def отдельно, а прямо в месте применения. Если вы используете какую-то функцию многократно, то лямбда-функция для нее вам, конечно, не нужна - лучше (читабельнее) определить эту функцию один раз с помощью def, а потом использовать ее в коде. Но если вам нужно на лету в одном конкретном месте сделать несложное превращение - лямбды очень удобны.

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

   1 f = lambda x: x*x

это будет эквиваленто:

   1 def f(x):
   2     return x*x

Обратите внимание на тонкости определения:

Еще пример (следующие функции идентичны):

   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) выражениях. Применение таких выражений мы рассмотрим в следующей части, а пока пример:

   1 >>> add_lambda = lambda a, b: a + b
   2 >>> add_lambda(3,6)
   3 9
   4 
   5 # а теперь просто подставим лямбду туда, где хотим ее использовать:
   6 >>> (lambda a, b: a + b)(3,6)  #конструкция (lambda ... : ...) "на лету" превращается в функцию, которую можно вызывать как обычную функцию, "со скобочками"
   7 9

Но реальная польза от лямбд есть - это их использование в функции 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 = [1,2,3,4,5]  # = range(1,6)
   2 squares = []
   3 
   4 for number in numbers:
   5     squares.append(number*number)
   6 
   7 # squares = [1,4,9,16,25]

* Вопрос на засыпку:

   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, и возвращает новый итерируемый объект. Её можно использовать здесь следующим образом:

   1 def square(x):
   2     return x*x
   3 numbers = range(1,6)
   4 squares = map( square, numbers )
   5 
   6 # squares = [1,4,9,16,25]

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

   1 square = lambda x: x*x
   2 numbers = range(1,6)
   3 squares = map( square, numbers )
   4 
   5 # squares = [1,4,9,16,25]

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

   1 numbers = range(1,6)
   2 squares = map( lambda x: x*x, numbers )  # просто подставим лямбду туда, где она нужна
   3 
   4 # squares = [1,4,9,16,25]

А теперь все то же самое, но без лямбд, и гораздо более читабельно:

   1 squares = [ x*x for x in range(1,6) ]
   2 
   3 # squares = [1,4,9,16,25]

Собственно это и есть генератор списка.

* не стоит сбрасывать map со счетов только потому, что есть такие клевые генераторы; он также удобен хотя бы потому, что есть его версия map_async, которая позволяет вам легко (практически не меняя код) выполнять параллельные вычисления, но в данном уроке мы эту тему рассматривать не будем

Так же, как и один цикл for можно перебирать два и более:

   1 mults = [ x*y for x in range(1,4) for y in range(1,3) ]
   2 
   3 # mults = [1, 2, 2, 4, 3, 6]  # более внешним является цикл, описанный более левым

Общий синтаксис генераторов:

[ result for var in iter if condition ], где:

iter - исходный перечисляемый объект (например, список или файл(!))

var - имя для переменной, в которую перебирается iter, эквивалентно for var in iter:

result - любое выражение, использующее var, которое должно быть подсчитано, а его результат - записан в новый список

condition - дополнительное условие, проверяющее что-то связанное с var - только соответствующие var будут обработаны, а остальные - отфильтрованы и выброшены.

   1 new_iter = [ result for var in iter if condition ]
   2 # эквивалентно:
   3 new_iter = []
   4 for var in iter:
   5     if condition:
   6         new_iter.append(result)

Например:

   1 squares = [ x*x for x in range(1,6) if x > 2 ]
   2 
   3 # squares = [9,16,25]

Только помните, что if перед condition - фильтрует переменные. Если вам нужно что-то вычислять по разному для разных var, но вывести все - вы можете использовать if внутри result. Это так называемый inline if: конструкция вида "a if b else c". Она эквивалентна:

   1 condition = ...
   2 results = 10 if condition else -10
   3 # эквивалентно
   4 if condition:
   5     results = 10
   6 else:
   7     results = -10

Удобство данной конструкции в том, что ее можно также, как и лямбды - вставлять в любое место кода, как цельное выражение, например:

   1 def abs(x):
   2     return x if x >= 0 else -x
   3 # читается в порядке:
   4 # return ((x) if (x >= 0) else (-x))
   5 # а НЕ:
   6 # (return x) if (x >= 0) else (-x)  # что такое -x в таком случае и куда уходит?

И теперь для вычисления разных результатов для разных элементов в генераторе списков мы можем подставить этот inline if:

   1 squares = [ x*x if x > 2 else -x for x in range(1,6) ]  # if теперь внутри result
   2 # эквивалентно:
   3 squares = []
   4 for x in range(1,6):
   5     if x > 2:
   6         squares.append(x*x)
   7     else:
   8         squares.append(-x)
   9 
  10 # squares = [-1,-2,9,16,25]

Генераторы словарей и сетов

Напомню, что словарь - это структура типа "ключ->значение", где ключи - уникальны; а сет - это такой "словарь без значений", где есть только одни ключи (что очень похоже на список, не запоминающий порядок элементов и содержащий только неповторяющиеся элементы). Хороши обе эти структуры тем, что поиск в них по ключу происходит мгновенно, сколько бы данных в них не хранилось, а вот поиск в списке будет идти тем дольше, чем больше у вас в списке элементов.

Так вот, точно так же, как и списки, можно генерировать словари и сеты (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), однако сам файл при этом в память целиком не грузится, а грузятся строки по одной, только тогда, когда они понадобились. Это очень хорошо, т.к. если ваш файл размером в несколько сотен гигабайт, то вы бы наверняка хотели избежать его загрузки в память целиком. Таким образом открытый файл - генератор: это итерируемый (перебираемый) объект, но при этом он не хранит в памяти все свое содержимое.

Вы тоже можете создавать такие объекты на питоне, и синтаксис его очень похож на генераторы списков:

   1 >>> squares = [ x*x for x in range(1,6) ]
   2 >>> squares
   3 [1,4,9,16,25]
   4 
   5 >>> squares_iter = ( x*x for x in range(1,6) )
   6 >>> squares_iter
   7 <generator object <genexpr> at 0x0249E238>
   8 >>> for i in squares_iter:
   9 ...     print(i)
  10 1
  11 4
  12 9
  13 16
  14 25

В данном случае генератор squares_iter - как раз пример "ленивых вычислений", т.к. каждый элемент этого итератора не вычисляется до тех пор, пока до него не дойдет дело при переборе. См. задание №3 для подробностей.

Однако помните! что такие итерируемые объекты требуют расплаты за то, что вам не нужно хранить в памяти их содержимое - а именно, к каждому их элементу можно обратиться только один раз!

   1 >>> squares_iter = ( x*x for x in range(1,6) )
   2 >>> squares_iter
   3 <generator object <genexpr> at 0x0249E238>
   4 >>> for i in squares_iter:
   5 ...     print(i)
   6 1
   7 4
   8 9
   9 16
  10 25
  11 >>> for i in squares_iter:  # один раз уже прошлись, всё!
  12 ...     print(i)
  13 >>>

Если вы хотите создать генератор, который сложнее, чем 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