Функциональное программирование на Python. Часть 7. Функции высшего порядка

Автор: | 06.07.2020

Функции высшего порядка – это один из мощнейших инструментов функционального подхода к программированию. Эта идея прекрасно реализована в Python, как на уровне самого языка, так и в его экосистеме: в модулях functools, operator и itertools.

Что такое функция высшего порядка?

На протяжении этого цикла статей мы неоднократно сталкивались с функциями высшего порядка (High Order Functions или HOF), явно про это написано в статье “ФП на Python. Часть 2. Абстракция и композиция. Функции. Функция высшего порядка – это функция, которая может принимать в качестве аргумента другую функцию и/или возвращать функцию как результат работы. Так как в Python функции – это объекты первого класса, то они являются HOF, это свойство активно используется при разработке программного обеспечения. 

Встроенные функции высшего порядка

К встроенным функциям высшего порядка, которые можно использовать без импорта каких-либо библиотек, относятся map и filter.

Функция map принимает функцию и итератор, возвращает итератор, элементами которого являются результаты применения функции к элементам входного итератора.

Примеры:

>>> a = [1, 2, 3, 4, 5]
>>> list(map(lambda x: x**2, a))
[1, 4, 9, 16, 25]

>>> def fun(x):
        if x % 2 == 0:
            return 0
        else:
            return x*2

>>> list(map(fun, a))
[2, 0, 6, 0, 10]

Функция filter принимает функцию предикат и итератор, возвращает итератор, элементами которого являются данные из исходного итератора, для которых предикат возвращает True.

Примеры:

>>> list(filter(lambda x: x > 0, [-1, 1, -2, 2, 0]))
[1, 2]

>>> list(filter(lambda x: len(x) == 2, ["a", "aa", "b", "bb"]))
['aa', 'bb']

Если придерживаться “питонического” стиля программирования, то вместо map и filter лучше использовать списковое включение (list comprehensions) с круглыми скобками  (в этом случае будет создан генератор, более подробно см. “Python. Урок 7. Работа со списками (list)“):

Вариант с функцией map:

map(lambda x: x**2, [1,2,3])

можно заменить на:

(v**2 for v in [1,2,3])

Для варианта с  функцией filter:

filter(lambda x: x > 0, [-1, 1, -2, 2, 0])

аналог будет выглядеть так:

(v for v in [-1, 1, -2, 2, 0] if v > 0)

В экосистеме Python есть два модуля: functools и operator, которые развивают идею использования HOF и предоставляют инструменты для разработки программ в функциональном стиле.

Модуль functools

Модуль functools предоставляет набор декораторов и HOF функций, которые либо принимают другие функции в качестве аргумента, либо возвращают функции как результат работы.

HOF функции

partial

Функция partial создает частично примененные функции (см. “Функция как возвращаемое значение. Каррирование, замыкание, частичное применение). Суть идеи в том, что если у функции есть несколько аргументов, то можно создать на базе нее другую, у которой часть аргументов будут иметь заранее заданные значения.

Прототип:

partial(func, /, *args, **keywords)

Параметры:

  • func
    • Функция, для которой нужно построить частично примененный вариант.
  • args
    • Позиционные аргументы функции.
  • keywords
    • Именованные аргументы функции.

Примеры:

В статье “ФП на Python. Часть 2. Абстракция и композиция. Функциимы приводили пример функции, которая складывает три переданных в нее аргумента и строили для нее частично примененный вариант, немного модифицируем ее:

def faddr(x, y, z):
   return x + 2 * y + 3 * z

Построим частично примененную функцию с помощью partial():

p_faddr = partial(faddr, 1, 2)
>>> p_faddr(3)
14

Построим функцию с заранее заданными значениями для параметров y и z:

>>> p_kw_faddr = partial(faddr, y=3, z=5)
>>> p_kw_faddr(2)
23

partialmethod

Инструмент, аналогичный по своему назначению функции partial(), применяется для методов классов.

Прототип:

class partialmethod(func, /, *args, **keywords)

Параметры:

  • func
    • Метод класса, для которого нужно построить частично примененный вариант.
  • args
    • Позиционные аргументы метода.
  • keywords
    • Именованные аргументы метода.

Примеры:

class Math:
   def mul(self, a, b):
       return a * b

   x10 = partialmethod(mul, 10)

>>> m = Math()
>>> m.mul(2,3)
6

>>> m.x10(5)
50

reduce

Сворачивает переданную последовательность с помощью заданной функции. Прототип функции:

reduce(function, iterable[, initializer])

Параметры:

  • function
    • Функция для свертки исходной последовательности, должна принимать два аргумента.
  • iterable
    • Последовательность для свертки (итератор).
  • initializer
    • Начальное значение, которое будет использоваться для сверки. Если значение не задано, то в качестве начального будет выбран первый элемент из итератора.

Примеры:

>>> from operator import add

>>> add(1,2)
3

>>> reduce(add, [1,2,3,4,5])
15

update_wrapper

Оборачивает исходную функцию так, чтобы она выглядела как функция-обертка.

Прототип:

update_wrapper(wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)

Параметры:

  • wrapper
    • Исходная функция.
  • wrapped
    • Функция обертка.
  • assigned
    • Кортеж с атрибутами, которые необходимо заместить у оборачиваемой функции атрибутами функции-обертки. Значение по умолчанию: WRAPPER_ASSIGNMENTS, в этом случае будут замещены атрибуты: __module__, __name__, __qualname__, __annotations__,  __doc__.
  • updated
    • Кортеж с атрибутами, которые необходимо заместить у функции-обертки атрибутами оборачиваемой функции. Значение по умолчанию: WRAPPER_UPDATES, в этом случае будут замещен атрибут: __dict__.

Примеры:

def x10(a):   
   return a * 10

def some_mul(a: int) -> int:
   """a * some value"""
   return a * 1

wrapped_mul = update_wrapper(x10, some_mul)

>>> wrapped_mul(3)
30

>>> wrapped_mul.__name__
'some_mul'

>>> wrapped_mul.__annotations__
{'a': <class 'int'>, 'return': <class 'int'>}

>>> wrapped_mul.__doc__
'a * some value'

Декораторы

@cached_property

Декоратор, который позволяет создавать свойства класса с поддержкой кэширования. Это может быть полезно, если обращение к свойству является дорогостоящей операцией с точки зрения затраты ресурсов. Доступна, начиная с Python 3.8.

Прототип:

@cached_property(func)

Параметры:

  • func
    • Декорируемая функция.

Примеры:

Напишем реализацию класса, который будет хранить набор данных и предоставлять свойство mean – среднее арифметическое. Для того, чтобы каждый раз не вычислять это значение при обращении к свойству, его можно объявить с декоратором @cached_property:

import functools

class DataProc:
   def __init__(self, data_set):
      self._data_set = data_set

   @functools.cached_property
   def mean(self):
      return sum(self._data_set) / len(self._data_set)

>>> d = DataProc([1,2,3,4,5])
>>> d.mean
5

@lru_cache

Декоратор для создания кэшированной версии функции (метода класса). 

Прототип:

@lru_cache(user_function)

@lru_cache(maxsize=128, typed=False)

Параметры:

  • maxsize
    • Количество запоминаемых результатов работы функции для разных наборов значений аргументов. Значение по умолчанию: 128.
  • typed
    • Если параметр равен True, то при кэшировании также будет учитываться тип аргументов.

Подробное описание:

Декоратор обеспечивает хранение результатов работы функции на различных аргументах в количестве до maxsize штук. Декоратор @lru_cache создает функцию cache_info(), с помощью которой можно оценить насколько эффективно работает LRU кэш, она возвращает именованный кортеж с четырьмя полями: hits (количество попаданий), misses (количество промахов), maxsize (максимальный размер кэша) и currsize (текущий размер кэша). Для очистки кэша используйте функцию cache_clear(). Если параметр typed=True, то при работе с кэшем будет учитываться тип аргумента, в этом случае func(10) и func(10.0) будут распознаваться как вызовы на разных аргументах (будут закэшированны по отдельности).

Примеры:

@lru_cache(maxsize=16)
def square(value):
   return value**2

>>> for v in [1, 2, 3, 4, 2, 3, 4, 5, 5, 6]:
...     square(v)
... 
1
4
9
16
4
9
16
25
25
36

>>> square.cache_info()
CacheInfo(hits=4, misses=6, maxsize=16, currsize=6)

@total_ordering

Декоратор класса, который автоматически добавляет методы сравнения, если задан одни из методов  __lt__(), __le__(), __gt__(), __ge__() и метод __eq__().

Прототип:

@total_ordering

Примеры:

Создадим класс для работы с рациональными (дробными) числами:

class Rational:
   def __init__(self, a, b):
       self.num = a
       self.den = b

   def __lt__(self, other):
       return (self.num / self.den) < (other.num / other.den)

   def __eq__(self, other):
       return (self.num == other.num) and (self.den == other.den)

В представленном варианте реализации можно использовать операции сравнения <, >, ==:

>>> a = Rational(1, 2)
>>> b = Rational(3, 4)

>>> a < b
True

>>> a == b
False

>>> a > b
False

Но использовать операции <=, >= нельзя:

>>> a <= b
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: '<=' not supported between instances of 'Rational' and 'Rational'

Если при объявлении класса добавить декоратор @total_ordering, то мы получим в распоряжение весь набор операторов сравнения:

@total_ordering
class Rational:
   def __init__(self, a, b):
       self.num = a
       self.den = b

   def __lt__(self, other):
       return (self.num / self.den) < (other.num / other.den)

   def __eq__(self, other):
       return (self.num == other.num) and (self.den == other.den)

Продемонстрируем это:

>>> a = Rational(1, 2)
>>> b = Rational(3, 4)

>>> a < b
True

>>> a <= b
True

>>> a == b
False

>>> a >= b
False

>>> a > b
False

@singledispatch

Декоратор трансформирует функцию в single-dispatch функцию. Особенность такой функции состоит в том, что выбор ее реализации определяется типом первого аргумента. Аналогичным по функционалу является singledispatchmethod.

Прототип:

@singledispatch

Примеры:

Создадим generic-фукцию с декоратором @singledispatch:

@singledispatch
def test(arg):
   print(f"Generic, arg: {arg}")

Пример добавления реализации с использованием аннотации типов:

@test.register
def _(arg: int):
   print(f"Int, arg: {arg}")

Пример явного задания типа через функцию register().

@test.register(float)
def _(arg):
   print(f"Float, arg: {arg}")

Пример работы с лямбда выражением:

test.register(str, lambda x: f"Str, arg: {x}")

Добавление уже созданной функции:

def list_printer_already_exist(arg):
   print(f"List, arg {arg}")

test.register(list, lambda x: f"List, arg: {x}")

Демонстрация работы функции test() на разных типах аргументов:

>>> test(None)
Generic, arg: None

>>> test(1)
Int, arg: 1

>>> test(2.0)
Float, arg: 2.0

>>> test("hello")
'Str, arg: hello'

>>> test([1,2,3])
'List, arg: [1, 2, 3]'

@wraps

Декоратор для упрощения работы с функцией update_wrapper().

Прототип:

@wraps(wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)

Описание параметров функции аналогично приведенным для update_wrapper.

Примеры:

def some_mul(a: int) -> int:
   """a * some value"""
   return a * 1

@wraps(some_mul)
def x20(a):
   return a * 20

>>> x20(3)
60

>>> x20.__name__
'some_mul'

>>> x20.__annotations__
{'a': <class 'int'>, 'return': <class 'int'>}

>>> x20.__doc__
'a * some value'

Модуль operator

Модуль operator содержит эффективные реализации функций, которые часто используются как аргументы функций высших порядков.

Например, функция reduce из модуля functools, рассмотренного выше, первым аргументом принимает функцию, которую будет использовать для свертки. Ее можно создать где-то предварительно или по месту (в виде лямбды) либо, если она есть в составе модуля operator, воспользоваться им:

>>> from operator import mul
>>> from functools import reduce

>>> reduce(lambda a, b: a * b, [1,2,3,4,5])
120

>>> reduce(mul, [1,2,3,4,5])
120

Ниже представлена таблица с функциями из модуля operator.

Операция Синтаксис Функция
Сложение a + b add(a, b)
Конкатенация seq1 + seq2 concat(seq1, seq2)
Тест на вхождение obj in seq contains(seq, obj)
Деление a / b truediv(a, b)
Деление a // b floordiv(a, b)
Побитовое И a & b and_(a, b)
Побитовое исключающее ИЛИ a ^ b xor(a, b)
Битовая инверсия ~ a invert(a)
Побитовое ИЛИ a | b or_(a, b)
Возведение в степень a ** b pow(a, b)
Проверка того, что a есть b a is b is_(a, b)
Проверка того, что a не есть b a is not b is_not(a, b)
Присвоение значения элементу  по его индексу obj[k] = v setitem(obj, k, v)
Удаление элемента по его индексу del obj[k] delitem(obj, k)
Получение элемента по его индексу obj[k] getitem(obj, k)
Сдвиг влево a << b lshift(a, b)
Остаток от деления a % b mod(a, b)
Умножение a * b mul(a, b)
Умножение матриц a @ b matmul(a, b)
Получение отрицательной версии числа – a neg(a)
Логическое НЕ not a not_(a)
Получение положительной версии числа + a pos(a)
Сдвиг вправо a >> b rshift(a, b)
Присвоение значений срезу последовательности seq[i:j] = values setitem(seq, slice(i, j), values)
Удаление среза элементов del seq[i:j] delitem(seq, slice(i, j))
Получение среза seq[i:j] getitem(seq, slice(i, j))
Форматирование строки s % obj mod(s, obj)
Вычитание a – b sub(a, b)
Проверка истинности obj truth(obj)
Операция порядка a < b lt(a, b)
Операция порядка a <= b le(a, b)
Проверка равенства a == b eq(a, b)
Проверка неравенства a != b ne(a, b)
Операция порядка a >= b ge(a, b)
Операция порядка a > b gt(a, b)

Модуль operator содержит версии функций, модифицирующие значение аргументов, в рамках данной статьи они рассматриваться не будут.

P.S.

Вводные уроки по “Линейной алгебре на Python” вы можете найти соответствующей странице нашего сайта. Все уроки по этой теме собраны в книге “Линейная алгебра на Python”.
Книга: Линейная алгебра на Python
Если вам интересна тема анализа данных, то мы рекомендуем ознакомиться с библиотекой Pandas.  Для начала вы можете познакомиться с вводными уроками. Все уроки по библиотеке Pandas собраны в книге Pandas. Работа с данными”.
Книга: Pandas. Работа с данными

Поделиться
Share on VK
VK
Tweet about this on Twitter
Twitter
Share on Facebook
Facebook

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *