Содержание
Функции высших порядков
Что такое функции высших порядков
FIXME: Функция первого порядка – это функция, на вход которой подаются объекты, которая возвращает объекты. Т.е. это обычная функция. Все функции, которые мы писали до сих пор, были функциями первого порядка.
FIXME: В питоне функция – это объект. О функции можно думать, как об объекте, который хранит в себе тело функции и знает, какие
- операцию
"вызвать функцию". А раз функция – это объект, то её можно сохранить в переменную или передать в качестве аргумента функции.
FIXME: Функция второго порядка – это функция, которая получает на вход функцию первого порядка или возвращает функцию первого порядка. Аналогично определяется функция третьего, четвёртого порядка, и т.п.
FIXME: Обычно не различают функции второго, третьего и т.д. порядков, а говорят о функциях высшего порядка – о функциях, которые оперируют какими-либо функциями.
Простой пример
Предположим, мы хотим описать объект "точка", чтобы потом уметь её рисовать. У этого объекта будет цвет и координаты:
И теперь мы хотим для нашей точки определить кучу методов:
- подвинуть её,
- повернуть её вокруг какой-нибудь точки на сколько-то градусов (для одной точки эта операция сведётся к тому, чтобы её куда-нибудь подвинуть, но если мы эту операцию применяем сразу ко многим точкам, то она будет выглядеть как вращение всей плоскости),
- растянуть от какой-то точки (т.е. аналогично, применить растяжение от заданного центра ко всей плоскости)
- применить инверсию (опять-таки, к плоскости, на которой лежит точка)
У всех этих методов есть одно общее свойство, они все сводятся к простейшим операциями из линейной алгебры и в любом пакете для линейной алгебры в питоне (например, numpy) соответствующие преобразования для векторов есть. Было бы глупо переписывать все эти операции заново.
Поэтому мы можем в нашем классе реализовать один метод: применить к координатам нашей точки какое-нибудь преобразование. Такой метод получает на вход преобразование. Преобразование – это функция, которая получает координаты на вход и возвращает координаты на выходе:
Мы видим, что с точки зрения использования всё просто: дописываем к переменной круглые скобки и тем самым вызываем функцию, которая в этой переменной лежит. Точно так же, как мы делали и раньше.
Как этим методом пользоваться? В простейших случаях тоже очень просто:
Главное наблюдение: когда мы функцию упоминаем без скобок после её имени, мы имеем дело с объектом этой функции.
Как получить объект функции
Или, другими словами, что передавать функции высшего порядка.
Не вызывать функцию
Это простейший вариант, который мы как раз рассмотрели в примере.
lambda
Для описания функций в питоне есть две конструкции: def, с которым мы уже давно знакомы, и lambda. lambda – способ несколько сокращённо описывать очень простые функции.
Выражение
1 a = lambda args: b
в точности эквивалентно выражению:
То есть lambda отличается от def тремя вещами:
lambda может описывать только функции, состоящие из одного выражения, значение которого сразу возвращается
lambda не привязывает функцию к имени, а вместо этого сразу возвращает объект функции – мы можем сразу же передать его в качестве аргумента другой функции
lambda записывается сильно короче
Прошлый пример мы можем переписать так:
Замыкания
Идея передавать функции в качестве параметров функциям пришла в питон из функционального программирования. (Наиболее популярные языки функционального программирования – это Lisp, Haskell, Ocaml, Erlang).
В функциональном программировании вводят понятие замыкания – это функция, с которой проассоциирован набор известных значений для её аргументов или для глобальных переменных, которые в ней используются.
В питоне есть несколько способов создавать замыкания, и на примерах этих способов я постараюсь разъяснить это понятие.
Замыкание на объекте
FIXME: Если у нас есть объект a и у него есть метод b, которая получает 3 параметра, из них первый self, то в тот момент, когда мы говорим a.b, мы получаем функцию, которая получает 2 параметра, потому, что self уже зафиксирован.
В питоне получить из объекта метод неизменённым труднее, чем сделать замыкание на объекте.
Замыкание на стеке
FIXME: А вот хотим мы сделать универсальные операции вращения или сдвига точки.
1 def shift(dx, dy):
2 def transformation(coords):
3 x, y = coords
4 new_coords = (x + dx, y + dy)
5 return new_coords
6 return transformation
7
8 def rotation(center, phi):
9 rotmatrix = numpy.matrix([[cos(phi), -sin(phi)],[sin(phi), cos[phi]])
10 center = numpy.array(center)
11 def transformation(coords):
12 shifted = numpy.array(coords)- center
13 rotated = numpy.dot(rotmatrix, shifted)
14 unshifted = rotated + center
15 return unshifted
16 return transformation
FIXME: обе функции на примере возвращают функции => они функции высшего порядка; функции transformation в обоих случаях используют данные, которых внутри самой функции нет, но они есть во внешней функции;
FIXME: сделано это так: когда мы создаём функцию, в ней мы запоминаем, что для неё является глобальным пространством имён; в данном случае это namespace другой функции;
FIXME: тут картинка на тему
FIXME: следствие: внутри функции будут использованы данные по состоянию на тот момент, когда мы вышли из внешней функции, т.е. такое не имеет смысла:
Т.е. в этом примере вызов f после второго присваивания будет озвращать 2.
Частичное применение
FIXME: Но если, положим, у нас уже есть функции для вращения (которая получает на вход два вектора и угол) и сдвига (которая получает на вход два вектора), то писать такие большие обёртки ради того, чтобы их исползовать, кажется несколько избыточным.
Пусть они выглядят примерно так:
У нас есть примерно два варианта, как поступить в этом случае:
Начиная с питона 2.5 в питоне появился модуль functools, и в нём есть функция partial для частичного применения.
Функция partial получает на вход функцию и сколько-нибудь аргументов к ней и возвращает функцию, которая принимает все необходимые аргументы, кроме заданных.
Встроенные функции высшего порядка
Всё в том же функциональном программировании когда речь заходит о функциях высшего порядка, есть несколько типичных операций, к которым их зачастую сводят.
map
FIXME: map – применить функцию к каждому элементу списка и вернуть получившийся список; map(f, a) – синоним [f(x) for x in a]
FIXME: в питоне списков может быть больше одного, тогда функция будет применяться к очередному элементу каждого из списков; map(None, a, b, c) отличается от zip(a, b, c) тем, что zip останавливается там, где кто-нибудь кончился, а map останавливается там, где кончились все.
filter
FIXME: filter – вернуть из списка только те элементы, для которых функция вернула истинное значение; filter(f, a) – синоним [x for x in a if f(x)]
List comprehensions и достаточно неказистая модель работы с функциями высшего порядка в питоне делают эти две функции довольно мало полезными. Единственное частое применение у меня: "".join(map(str, items))
reduce
FIXME: Приблизительное определение reduce: reduce(f, [a,b,c,d]) = f(f(f(a,b), c), d); reduce(f, [a,b,c], z) = f(f(f(z, a), b), c) Это весьма головоломная функция, с которой просто работать только в одном случае – когда f – коммутативная операция. Например, reduce(lambda a,b: a+b, list) = sum(list).
Очевидно, и от этой функции пользы довольно мало, и хотя она и позволяетя написать код несколько компактнее, зачастую, лучше ей не пользоваться.
В питоне 3 эта функция переехала в модуль functools, поэтому в питоне 2.6 и моложе она есть и как самостоятельная функция, и в модуле functools.
operator
Именно для простоты использования в фукциях высшего порядка (и в особенности для всяких приятностей типа частичного применения), в питоне сделали модуль operator, в котором есть легкопроизносимые названия для встроенных операций. Например, в нём есть функция add(a,b), которая является дословным синонимом a+b – и аналогично для всех операций, какие есть в питоне.
План
как: функция – тоже объект
lambda
map
filter
reduce (vs. sum)
functools:
operator
Примеры
К прошлой теме:
side_chain = filter(lambda atom: atom.name not in side_chain_names, atoms)
word = map(str.upper, word)
class Description: ...; library = map(Description, open("..."))
class Residue: ...; ids = map(Residue.short_id, residues)
for a, b in map(lambda v: v[:2], parse_csv(file))
",".join(map(str, ints))
map(float, line.split())
map(lambda s: s.strip().strip("\"'"), line.split(","))
all(map(issubclass, map(type, args), p))
sum(map(len, fields)) + 2 * len(fields)
filter(None, values)[0]
codes = {...}; aa_code = map(codes.get, seq)
zip(*map(parse, [line1, line2]))
sum(map(seq.count, ['G', 'C']))
vs.
x = [...]; map(x.remove, y)