Функции высшего порядка – это один из мощнейших инструментов функционального подхода к программированию. Эта идея прекрасно реализована в Python, как на уровне самого языка, так и в его экосистеме: в модулях functools, operator и itertools.
Функция высшего порядка – это функция, которая может принимать в качестве аргумента другую функцию и/или возвращать функцию как результат работы.
Объекты первого класса
В Python функции являются объектами первого класса. Это означает, что их можно передавать в качестве аргументов другим функциям, возвращать значения из функций и хранить в переменных или структурах данных как любой другой объект. Это позволяет использовать функции как строительные блоки для более сложных программ.
Напишем пример, чтобы посмотреть, как функции используются в качестве объекта первого класса:
def hello(name):
print(f"Hello, {name}!")
greeting_function = hello
greeting_function("Hexlet")
# "Hello, Hexlet!"
В этом примере мы определяем функцию hello
, которая принимает один аргумент и печатает приветствие. Затем мы присваиваем функцию переменной greeting_function
и вызываем ее с аргументом.
Рассмотрим другой пример с передачей функции в качестве аргумента другой функции:
def apply_function(numbers, function):
results = []
for number in numbers:
result = function(number)
results.append(result)
return results
def square(number):
return number ** 2
numbers = [1, 2, 3, 4, 5]
squared_numbers = apply_function(numbers, square)
print(squared_numbers)
# [1, 4, 9, 16, 25]
В этом примере функция apply_function
принимает в качестве аргументов список чисел и функцию. Функция apply_function
применяет переданную функцию к каждому числу в списке и возвращает новый измененный список. Функция square
возводит число в квадрат, и используется в качестве аргумента функции apply_function
.
Объекты первого класса также позволяют возвращать функции из другой функции:
def make_multiplier(n):
def multiplier(x):
return x * n
return multiplier
times_2 = make_multiplier(2)
times_3 = make_multiplier(3)
print(times_2(5)) # 10
print(times_3(5)) # 15
В этом примере функция make_multiplier
принимает число n
и возвращает новую функцию, которая умножает свой аргумент на n
. Затем эта функция применяется, чтобы создать две новые функции times_2
и times_3
, которые умножают свой аргумент на 2 и 3 соответственно. В итоге вызывается функция с аргументом 5, чтобы увидеть их результаты.
Функции высшего порядка
Функции, которые принимают другие функции в качестве аргументов и/или возвращают функции в качестве результатов, называются функциями высшего порядка или ФВП. Их можно использовать для инкапсуляции многократно используемого поведения и создания более абстрактного кода, о котором легче рассуждать.
Например, встроенные функции map
и filter
в Python являются функциями высшего порядка, которые работают с итерируемыми объектами и применяют функцию к каждому элементу итерируемого объекта.
Рассмотрим следующий пример функции высшего порядка:
def repeat(func, n):
for i in range(n):
func()
def hello():
print("Hello, world!")
repeat(hello, 3)
Вывод будет следующим:
Hello, world!
Hello, world!
Hello, world!
Этот код использует функцию repeat()
для многократного вызова функции hello()
.
Рассмотрим другой пример с возвратом функции:
def double(function):
def inner(argument):
return function(function(argument))
return inner
def multiply_by_five(x):
return x * 5
double(multiply_by_five)(3)
# 75
В этом примере в теле функции double
создается функция inner
и возвращается в роли результата. Так как вызов double
возвращает функцию, мы можем сразу сделать второй вызов ((3)
). Он уже даст результат двойного применения исходной функции к аргументу.
Имя inner
довольно часто используется, чтобы называть функции, которые создаются на лету внутри внешней функции.
Но мы могли бы и не вызвать функцию-значение сразу, а вместо этого сохранить в переменную:
multiply_by_25 = double(multiply_by_five)
multiply_by_25
# <function double.<locals>.inner at 0x7fd1975c58c8>
multiply_by_25(1)
# 25
multiply_by_625 = double(multiply_by_25)
multiply_by_625
# <function double.<locals>.inner at 0x7fd1968f41e0>
multiply_by_625(1)
# 625
При выводе значения ссылки multiply_by_25
отображается double.<locals>.inner
— та самая созданная на лету функция inner
.
И в случае multiply_by_625
функция называется inner
, но адрес в памяти другой — большое шестнадцатеричное число после “at”.
Встроенные функции высших параметров
В Python есть функции, одним из аргументов которых являются другие функции: map()
, filter()
, reduce()
, apply()
.
Три наиболее общих функций высшего порядка встроены в Python: map (), reduce () и filter (). Эти функции используют в качестве (некоторых) своих параметров другие функции — вот почему мы называем их функциями высшего порядка.
map()
Функция map()
позволяет обрабатывать одну или несколько последовательностей с помощью заданной функции:
>>> list1 = [7, 2, 3, 10, 12] >>> list2 = [-1, 1, -5, 4, 6] >>> 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] Следующий пример: Принимает функцию-аргумент и применяет её ко всем элементам входящей последовательности. # напечатаем квадраты чисел от 1 до 5 my_list = list(map(lambda x: x**2, [1, 2, 3, 4, 5])) print(my_list) > [1, 4, 9, 16, 25]
filter()
Функция filter()
позволяет фильтровать значения последовательности. В результирующем списке только те значения, для которых значение функции для элемента истинно:
>>> numbers = [10, 4, 2, -1, 6] >>> 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] Следующий пример: Как следует из названия, filter() фильтрует последовательность по заданному условию. # отфильтруем список с целью получить только чётные значения my_list = list(filter(lambda x: x % 2 == 0, [11, 22, 33, 44, 55, 66])) print(my_list) > [22, 44, 66]
zip()
Упаковывает итерируемые объекты в один список кортежей. При работе ориентируется на объект меньшей длины:
word_list = ['Elf', 'Dwarf', 'Human']
digit_tuple = (3, 7, 9, 1)
ring_list = ['ring', 'ring', 'ring', 'ring', 'ring']
my_list = list(zip(word_list, digit_tuple, ring_list))
print(my_list)
> [('Elf', 3, 'ring'), ('Dwarf', 7, 'ring'), ('Human', 9, 'ring')]
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
Модуль functools
Functools — это библиотека, которая содержит дополнительные функции высших порядков. Подключай и используй.
reduce()
Для организации цепочечных вычислений в списке можно использовать функцию reduce()
. Например, произведение элементов списка может быть вычислено так (Python 2):
>>>from functools import reduce >>>numbers = [2, 3, 4, 5, 6] >>> reduce(lambda res, x: res*x, numbers, 1) 720
Вычисления происходят в следующем порядке:
(((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]
Принимает функцию и последовательность. Запускает цепь вычислений, применяя функцию ко всем элементам последовательности. Сводит набор к единственному значению.
from functools import reduce
# посчитаем сумму элементов списка
res = reduce(lambda sum, x: sum + x, [0.1, 0.3, 0.6])
print(res)
> 1.0
С помощью reduce()
можно делать так:
import functools
# посчитаем с reduce количество вхождений строки в список
uncle_ben = ['С большой силой', 'приходит', 'большая ответственность']
complete = functools.reduce(lambda a, x: a + x.count('ответственность'), uncle_ben, 0)
print(complete)
> 1
partial()
Функция служит для частичного назначения аргументов. На входе и на выходе — тоже функции.
import functools
# допустим у нас есть какая-то функция
def set_stars(amount, type):
print(type, amount)
# уменьшаем число аргументов set_stars с partial
print_neutron_star = functools.partial(set_stars, type='Neutron stars: ')
print_neutron_star(1434)
> Neutron stars: 1434
cmp_to_key()
Возвращает ключевую функцию для компарации объектов.
# сортировка по убыванию
def num_compare(x, y):
return y - x
print(sorted([4, 43, 1, 22], key=functools.cmp_to_key(num_compare)))
> [43, 22, 4, 1]
update_wrapper()
Используется для обновления метаданных функции-обертки, данными из некоторых атрибутов оборачиваемой функции. Обеспечивает лучшую читаемость и возможность повторного использования кода.
Модуль functools
предлагает множество полезных функций более высокого порядка, которые взаимодействуют с другими функциями или возвращают их. Эти функции можно использовать для реализации кэширования функций/методов, перегрузки, создания декораторов и в целом для того, чтобы сделать код немного более чистым.
Кэширование значений.
Кэширование результатов представлено тремя функциями (можно использовать в качестве декоратора)- functools.lru_cache()
,- functools.cache()
,- functools.cached_property()
.
Первая из них – @functools.lru_cache()
запоминает возвращаемый результат в соответствии с переданными аргументами. Такое поведение может сэкономить время и ресурсы:
from functools import lru_cache import requests @lru_cache(maxsize=32) def get_with_cache(url): try: r = requests.get(url) return r.text except: return "Not Found" for url in ["https://google.com/", "https://yandex.ru/", "https://mail.ru/", "https://google.com/", "https://yandex.ru/", "https://google.com/"]: get_with_cache(url) print(get_with_cache.cache_info()) # CacheInfo(hits=3, misses=3, maxsize=32, currsize=3) print(get_with_cache.cache_parameters()) # {'maxsize': 32, 'typed': False}
В этом примере, с помощью декоратора @lru_cache
выполняются GET-запросы и кэшируются их результаты (до 32 кэшированных результатов). Чтобы убедиться, что кэширование действительно работает, можно проверить информацию о кэше, используя метод .cache_info()
, который показывает количество попаданий и промахов в кэш. Декоратор также предоставляет методы .clear_cache()
и .cache_parameters()
для аннулирования кэшированных результатов и проверки параметров соответственно.
Если необходимо иметь немного более продвинутое кэширование, то можно включить необязательный аргумент typed=True
, который заставляет аргументы разных типов кэшироваться отдельно.
Еще одним декоратором кэширования в functools
является функция, называемая просто functools.cache()
. Это простая обертка поверх functools.lru_cache()
, в которой отсутствует аргумент max_size
, т.е. она не ограничивает количество кэшированных значений.
Декоратор @functools.cached_property()
используется для кэширования результатов атрибутов класса. Это очень полезно, если есть свойство, которое дорого вычислять, но при этом является неизменяемым. Работает аналогично встроенной функции property()
с добавлением кэширования.
from functools import cached_property class Page: @cached_property def render(self, value): # Что-то делается с аргументом `value`, что приводит к дорогому # вычислению, и как следствие отображение HTML-страницы. # ... return html
Этот простой пример показывает, как можно использовать functools.cached_property()
, например, для кэширования отрендеренной HTML-страницы, которая будет возвращаться пользователю снова и снова. То же самое можно сделать для определенных запросов к базе данных или длительных математических вычислений.
Предупреждение для всех кэширующих декораторов: не используйте кэширующие декораторы, если функция имеет какие-либо побочные эффекты или если она создает изменяемые объекты при каждом вызове.
Примечание. Побочный эффект функции – возможность в процессе выполнения своих вычислений: читать и модифицировать значения глобальных переменных, осуществлять операции ввода-вывода, реагировать на исключительные ситуации, вызывать их обработчики.
Автоматическая реализация операторов сравнения.
В Python можно реализовать операторы сравнения, такие как <
, >=
или ==
, используя __lt__
, __gt__
или __eq__
. Однако реализация каждого из __eq__
, __lt__
, __le__
, __gt__
или __ge__
может быть довольно раздражающей. Модуль functools
включает в себя декоратор @functools.total_ordering()
, который может помочь в этом – все, что нужно сделать, это реализовать __eq__
и один из оставшихся методов, а остальные будут автоматически созданы декоратором:
from functools import total_ordering @total_ordering class Number: def __init__(self, value): self.value = value def __lt__(self, other): return self.value < other.value def __eq__(self, other): return self.value == other.value print(Number(20) > Number(3)) # True print(Number(1) < Number(5)) # True print(Number(15) >= Number(15)) # True print(Number(10) <= Number(2)) # False
Код показывает, что, несмотря на то, что реализованы только __eq__
и __lt__
, можно использовать все расширенные операции сравнения. Наиболее очевидным и важным здесь является сокращение кода и его читабельность.
Перегрузка методов/функций.
Всех учили, что перегрузка функций невозможна в Python, но на самом деле есть простой способ реализовать ее, используя две функции в модуле functools
– это functools.singledispatch()
и/или functools.singledispatchmethod()
. Эти функции помогают реализовать то, что называется алгоритмом множественной отправки, который позволяет языкам программирования с динамической типизацией, таким как Python, различать типы во время выполнения.
Перегрузка функций является большой темой, по этому смотрите отдельные материалы:
Передача функций обратного вызова.
Часто приходится работать с различными внешними библиотеками или фреймворками, многие из которых предоставляют функции и интерфейсы, требующие от нас передачи функций обратного вызова, например, для асинхронных операций или для прослушивателей событий. В этом нет ничего нового, но что, если встает необходимость передать вместе с функцией обратного вызова некоторые аргументы? Вот где пригодится functools.partial()
, ее можно использовать для замораживания некоторых (или всех) аргументов функции, создавая новый объект с упрощенной сигнатурой функции.
Рассмотрим несколько практических примеров:
def output_result(result, log=None): if log is not None: log.debug(f"Result is: {result}") def concat(a, b): return a + b import logging from multiprocessing import Pool from functools import partial logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger("default") p = Pool() p.apply_async(concat, ("Hello ", "World"), callback=partial(output_result, log=logger)) p.close() p.join()
Приведенный выше фрагмент демонстрирует, как можно использовать функцию partial()
для передачи функции output_result()
ее аргумента log=logger
в качестве функции обратного вызова callback
. Здесь используется multiprocessing.apply_async()
, которая асинхронно вычисляет результат предоставленной функции concat()
и возвращает ее результат функции обратного вызова. Но, метод .apply_async()
всегда будет передавать результат функции concat()
в качестве первого аргумента, а если нужно включить какие-либо дополнительные аргументы, например, как в данном случае log=logger
, то необходимо использовать functools.partial()
.
Это был довольно сложный вариант использования. Более простым примером может быть простое создание функции, которая вместо stdout
печатает в stderr
:
import sys from functools import partial print_stderr = partial(print, file=sys.stderr) print_stderr("This goes to standard error output")
С помощью этого простого трюка создали новую вызываемую функцию, которая всегда будет передавать аргумент с ключевым словом file=sys.stderr
для печати, что позволяет упростить код за счет отсутствия необходимости каждый раз указывать аргумент с ключевым словом.
Функция functools.partial()
можно также применить к малоизвестной особенности функции iter()
, например можно создать итератор, передав в iter()
вызываемую функцию и дозорное значение, что может быть полезно в следующем приложении:
from functools import partial RECORD_SIZE = 64 # Чтение двоичного файла... with open("file.data", "rb") as file: records = iter(partial(file.read, RECORD_SIZE), b'') for r in records: # Сделаем что-нибудь с записью...
Обычно, при чтении файла нужно перебирать строки, но в случае двоичных данных вместо строк приходится перебирать записи фиксированного размера. Это можно сделать, создав вызываемый объект с использованием функции partial()
, которая считывает указанный фрагмент данных и передает его итератору, который потом создает из него итератор. Затем этот итератор вызывает функцию чтения до тех пор, пока не будет достигнут конец файла, всегда принимая только указанный фрагмент данных RECORD_SIZE
. Наконец, когда достигнут конец файла, возвращается сигнальное значение b''
, и итерация останавливается.
Сохранение имени и строк документации декорированной функции.
При инспектировании кода, когда проверяется имя и строка документации декорированной функции, часто обнаруживается, что они были заменены значениями функции-декоратора. Такое поведение нежелательно, так как невозможно каждый раз переписывать все имена функций и строки документации, когда используется какой-либо декоратор. Это проблема решается при помощи декоратора functools.wraps()
:
from functools import wraps def decorator(func): @wraps(func) def actual_func(*args, **kwargs): """ Внутренняя функция внутри декоратора, которая выполняет фактическую работу """ print(f"Перед вызовом {func.__name__}") func(*args, **kwargs) print(f"После вызова {func.__name__}") return actual_func @decorator def greet(name): """Здоровается с кем-нибудь""" print(f"Hello, {name}!") print(greet.__name__) # greet print(greet.__doc__) # Здоровается с кем-нибудь
Единственная задача функции functools.wraps()
– скопировать имя, строку документа, список аргументов и т.д., для предотвращения их перезаписи. И учитывая, что functools.wraps()
также является декоратором, можно просто поместить его перед actual_func
, и проблема решена!
Функция functools.reduce()
.
Функция functools.reduce()
берет итерируемый объект и уменьшает (или складывает) все его значения в одно. У этого есть много разных применений, вот некоторые из них:
from functools import reduce import operator def product(iterable): return reduce(operator.mul, iterable, 1) def factorial(n): return reduce(operator.mul, range(1, n)) def sum(numbers): """Внимание! Переопределяется встроенная функция `sum()`""" return reduce(operator.add, numbers, 1) def reverse(iterable): return reduce(lambda x, y: y+x, iterable) print(product([1, 2, 3])) # 6 print(factorial(5)) # 24 print(sum([2, 6, 8, 3])) # 20 print(reverse("hello")) # olleh
Функция functools.reduce
может упростить и уменьшить код в одну строку, который в противном случае был бы намного длиннее. С учетом сказанного, чрезмерное использование этой функции только ради сокращения кода обычно является плохой идеей.
Кроме того, учитывая, что использование сокращения обычно приводит к однострочникам, то это идеальный кандидат для functools.partial()
:
from functools import reduce, partial import operator product = partial(reduce, operator.mul) print(product([1, 2, 3])) # 6
И, наконец, если нужен не только конечный сокращенный результат, но и промежуточный, то можно использовать itertools.accumulate()
– функцию из другого встроенного модуля itertools
. Вот как можно его использовать для вычисления максимального рабочего времени:
from itertools import accumulate data = [3, 4, 1, 3, 5, 6, 9, 0, 1] print(list(accumulate(data, max))) # [3, 4, 4, 4, 5, 6, 9, 9, 9]