Функции высшего порядка, замыкания и декораторы
Разберем важные концепции, связанные с функциями высшего порядка, напишем собственные версии map(), reduce() и filter(), потренируемся в создании декораторов и решим 10 практических заданий.
Функции высшего порядка
В программировании (и в математике) функциями высшего порядка называются функции, которые выполняют одно (или оба) из этих действий:
- Принимают одну (и более) функций в качестве аргументов.
- Возвращают функцию в качестве результата.
Все остальные функции считаются функциями первого порядка. Вот простейший пример обработки нескольких функций первого порядка multiply(), power(), add(), subtract() функцией высшего порядка higher_order():
def higher_order(function): # функция высшего порядка
return function(15)
def multiply(x): # функция первого порядка
return x * x
def power(x): # функция первого порядка
return x ** x
def add(x): # функция первого порядка
return x + x
def subtract(x): # функция первого порядка
return x - (x * x)
print(higher_order(multiply))
print(higher_order(power))
print(higher_order(add))
print(higher_order(subtract))
Вывод:
225
437893890380859375
30
-210
Декораторы в Python
Синтаксис Python позволяет использовать декораторы для получения результата «прохождения» функции первого порядка через функцию высшего порядка. Декоратор – это функция высшего порядка, которая принимает функцию первого порядка и добавляет в результат что-нибудь от себя, не вмешиваясь в логику полученной функции:
def print_result(f):
def result(x):
r = f(x)
print(f'Результат вычисления: {r}')
return r
return result
@print_result
def triple(x):
return x * 3
@print_result
def divide(x):
return x / 5
triple(5)
divide(5)
Вывод:
Результат вычисления: 15
Результат вычисления: 1.0
Более того, Python позволяет писать функции, которые создают декораторы:
def print_with(message):
def result(f):
def add_message(x):
r = f(x)
print(f'{message} {r}')
return r
return add_message
return result
@print_with('Функция вернула результат:')
def power(x):
return x ** x
power(int(input()))
Вывод для n = 5:
Функция вернула результат: 3125
Поскольку функции в Python являются объектами (класса function), при желании их можно добавлять в словари или списки:
def print_with(message):
def result(f):
def add_message(x):
r = f(x)
print(f'{message} {r}')
return r
return add_message
return result
functions = []
def function(f):
functions.append(f)
@function
@print_with('Функция вернула результат:')
def add(x):
return x + x
print(functions[0](6))
Вывод:
Функция вернула результат: 12
12
Порядок перечисления декораторов имеет значение – в приведенном выше примере в стек попала функция add(), уже измененная функцией высшего порядка print_with(). При изменении порядка декораторов результат будет просто 12
.
Декораторы очень часто используются при разработке приложений в Python фреймворках – они позволяют программисту использовать мощную функциональность фреймворка, не задумываясь о том, что именно происходит «под капотом». В приведенном ниже примере декоратор app.route обеспечивает маршрутизацию сайта на основе фреймворка Flask:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return 'Главная страница сайта'
@app.route('/hello')
def hello():
return 'Привет, добро пожаловать на сайт!'
if __name__ == '__main__':
app.run(host='127.0.0.1', port=8000, debug=True)
При запуске этого примера по адресу http://localhost:8000/ будет выведена надпись «Главная страница сайта», а по адресу http://localhost:8000/hello – «Привет, добро пожаловать на сайт!»
А в этом примере из функции представления фреймворка Django декоратор @login_required проверяет, вошел ли посетитель на сайт, и в зависимости от результата проверки либо показывает ему страницу, заполненную информацией, созданной этим конкретным пользователем, либо перенаправляет на страницу входа:
@login_required(login_url='/login')
def home(request):
all_tasks = request.user.tasks.all()
return render(request, 'index.html', {'tasks': all_tasks })
Как работают встроенные функции высшего порядка
В предыдущих главах мы уже неоднократно использовали три самые популярные встроенные функции высшего порядка – map(), reduce() и filter() для обработки наборов данных. В этом примере встроенная функция map() берет на себя ряд преобразований элементов строки:
>>> lst = list(map(float, input().split(';')))
1;2;3;4;5;6;7;8;9
>>> print(lst)
[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]
Если бы функции map() не было, пришлось бы заниматься этими преобразованиями самостоятельно – с помощью спискового включения:
sp = input().split(';')
result = [float(i) for i in sp]
print(result)
Или с помощью цикла:
sp = input().split(';')
result = []
for i in sp:
result.append(float(i))
print(result)
Встроенная функция map(), как и любая другая функция высшего порядка, может принимать любую функцию первого порядка и последовательно применять ее ко всем элементам в полученном наборе данных. В приведенном ниже примере встроенная map() принимает пользовательскую функцию divide():
sp = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
def divide(x):
return 1 / x ** 0.5
result = list(map(divide, sp))
print(result)
Вывод:
[1.0, 0.7071067811865475, 0.5773502691896258, 0.5, 0.4472135954999579, 0.4082482904638631, 0.3779644730092272, 0.35355339059327373, 0.3333333333333333, 0.31622776601683794]
Встроенная функция map() отличается гибкостью – точно такой же результат можно получить, если передать в нее анонимную лямбда-функцию:
sp = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
result = list(map(lambda x: 1 / x ** 0.5, sp))
print(result)
Напишем собственную функцию my_map(), которая будет принимать любую другую функцию первого порядка, например, my_function(), которая повторяет полученное число столько раз, чему оно равно:
def my_function(n):
lst = [str(n) for i in range(1, n + 1)]
return(''.join(lst))
def my_map(function, lst):
result = []
for i in lst:
processed_item = function(i)
result.append(processed_item)
return result
print(my_map(my_function, [4, 5, 6, 7]))
Вывод:
['4444', '55555', '666666', '7777777']
Как и встроенная map(), пользовательская my_map() может принимать анонимные функции:
def my_map(function, lst):
result = []
for i in lst:
processed_item = function(i)
result.append(processed_item)
return result
print(my_map(lambda x: ''.join([str(x) for i in range(1, x + 1)]), [1, 2, 5, 9]
Вывод:
['1', '22', '55555', '999999999']
Замыкания и вложенные функции
В программировании существует концепция, называемая замыканием (closure), когда вложенная функция имеет доступ к локальным переменным функции более высокого порядка, после того, как внешняя функция уже завершила свою работу:
def print_greetings(text):
def say_hello():
print(text)
return say_hello
print_hi = print_greetings('Привет, как дела?!')
print_hi()
Сохранение доступа к переменным функции более высокого порядка возможно благодаря своеобразному виртуальному контейнеру – стеку, принцип работы которого мы рассматривали в предыдущей статье. В приведенном ниже примере при вызове outer_function() в стеке сохраняется фрейм, в котором находятся вложенная функция inner_function() (как константа) и строка text_1
(как локальная переменная). Поскольку функция inner_function() ссылается на переменную text_1
, значение переменной остается доступным после того, как функция высшего порядка уже завершила свою работу.
def outer_function(text_1):
def inner_function():
text_2 = 'Это функция первого порядка - внутренняя'
print(text_2)
print(f'{text_1}, ee значение было сохранено во фрейме')
return inner_function
my_function = outer_function('Это функция высшего порядка - внешняя')
my_function()
Вывод:
Это функция первого порядка - внутренняя
Это функция высшего порядка - внешняя, ee значение было сохранено во фрейме
На практике замыкания используются для инкапсуляции кода и скрытия важных данных. С помощью замыканий также можно избежать использования глобальных переменных.
Важно заметить, что не всякая вложенная функция автоматически выступает в качестве замыкания, то есть сохраняет значения всех переменных из доступной области видимости: если во вложенной функции нет ссылок на какие-то переменные из функции высшего порядка, они не будут сохранены, и в этом случае вложенная функция считается просто вложенной функцией, а не замыканием. В приведенном ниже примере интерпретатор Python демонстрирует, что именно он посчитал замыканием, а что – нет:
>>> def higher_order():
text1 = 'Привет'
text2 = 'Учишь Python?'
def nested():
return text1
return nested
>>> my_function = higher_order()
>>> my_function
<function higher_order.<locals>.nested at 0x025B4390>
>>> my_function.__closure__
(<cell at 0x02578B30: str object at 0x025B17A0>,)
>>> my_function.__closure__[0].cell_contents
'Привет'
>>> higher_order.__closure__ is None
True
Значение переменной text2
не было сохранено в __closure__
, в отличие от использованного во вложенной функции nested() значения text1
– если выполнить команду my_function.__closure__[1].cell_contents
, получим ошибку, так как никаких других значений в кортеже нет:
Traceback (most recent call last):
File "<pyshell>", line 1, in <module>
IndexError: tuple index out of range
В этом можно также убедиться, выполнив команду по выводу имен переменных, ставших замыканиями:
>>> higher_order.__code__.co_cellvars
('text1',)
Практика
Задание 1
Напишите функцию высшего порядка, которая получает в качестве аргумента две функции первого порядка:
- Функцию для преобразования текста сообщения в верхний регистр.
- Функцию для преобразования текста сообщения в нижний регистр.
Пример вывода:
РЕГИСТР ПРЕОБРАЗОВАН ФУНКЦИЕЙ, ПОЛУЧЕННОЙ В КАЧЕСТВЕ АРГУМЕНТА
регистр преобразован функцией, полученной в качестве аргумента
Решение:
def greetings(function):
text = function('Регистр преобразован функцией, полученной в качестве аргумента')
print(text)
def uppercase(text):
return text.upper()
def lowcase(text):
return text.lower()
greetings(uppercase)
greetings(lowcase)
Задание 2
Напишите функцию высшего порядка, которая может принимать функции float(), hex(), bin(), str() и содержит вложенную функцию первого порядка, которая конвертирует полученное от пользователя целое число n в соответствии с полученными функциями.
Пример вывода для n = 25:
Преобразуем полученное число 25 в типы:
float => 25.0
bin => 0b11001
hex => 0x19
str => 25
Решение:
def number_to(function):
def convert(n):
return function(n)
return convert
to_float = number_to(float)
to_bin = number_to(bin)
to_hex = number_to(hex)
to_str = number_to(str)
n = int(input())
print(f'Преобразуем полученное число {n} в типы:'
f'\nfloat => {to_float(n)}'
f'\nbin => {to_bin(n)}'
f'\nhex => {to_hex(n)}'
f'\nstr => {to_str(n)}'
)
Задание 3
Напишите функцию высшего порядка, которая:
- Определяет, состоит ли полученный от пользователя список из четного или нечетного количества чисел.
- Возвращает функцию умножения элементов списка (в которой не используется math.prod()), если количество чисел четное.
- Возвращает функцию суммирования (в которой не используется встроенная функция sum()), если количество чисел нечетное.
Пример ввода 1:
8 9 3 5 1 3 8 2 9
Вывод 1:
Количество чисел нечетное, результат: 48
Пример ввода 2:
7 3 2 8 9 1 2 3 4 6
Вывод 2:
Количество чисел четное, результат: 435456
Решение:
def production(lst):
prod = 1
for i in lst:
prod *= i
return f'Количество чисел четное, результат: {prod}'
def summa(lst):
res = 0
for i in lst:
res += i
return f'Количество чисел нечетное, результат: {res}'
def higher_order(lst):
if len(lst) % 2 == 0:
return production
else:
return summa
sp = list(map(int, input().split()))
result = higher_order(sp)
print(result(sp))
Задание 4
Напишите собственный аналог функции filter(). Для отбора данных my_filter() должна, как и встроенная filter(), использовать функцию-предикат. Функция-предикат возвращает True
или False
в зависимости от критерия – в нашем случае это факт совпадения первой и последней букв слова в строке, полученной от пользователя.
Пример ввода:
крюк арбуз торт абрикос кулак барабан рупор господин томат мадам
Вывод:
крюк торт кулак рупор томат мадам
Решение 1:
def equal_letters(word):
return word[0] == word[-1]
def my_filter(function, line):
result = []
for word in line:
if function(word):
result.append(word)
return result
stroka = input().split()
print(*my_filter(equal_letters, stroka))
Решение 2:
def my_filter(function, line):
result = []
for word in line:
if function(word):
result.append(word)
return result
stroka = input().split()
print(*my_filter(lambda x: x[0] == x[-1], stroka))
Задание 5
Напишите собственный вариант функции-агрегатора reduce(). Функция при вызове должна получать:
- Функцию первого порядка для проведения операции умножения или сложения.
- Список чисел от пользователя.
- Начальное значение – 0 для операции суммирования, 1 для операции умножения.
Пример ввода:
5 7 8 3 2 5 8 12 3 5 4 8 9
Примеры вызова:
print(my_reduce(add, my_list, 0))
print(my_reduce(mult, my_list, 1))
Вывод:
79
3483648000
Решение:
def my_reduce(operation, lst, init):
result = init
for i in lst:
result = operation(result, i)
return result
def add(x, y):
return x + y
def mult(x, y):
return x * y
my_list = list(map(int, input().split()))
print(my_reduce(add, my_list, 0))
print(my_reduce(mult, my_list, 1))
Задание 6
В предыдущих статьях мы неоднократно использовали встроенную функцию zip() для параллельной итерации двух наборов данных. Напишите функцию высшего порядка, которая возвращает функции для:
- Группировки параллельных элементов списков с помощью самописной my_zip().
- Конкатенации элементов, сгрупированных my_zip().
- Сложения элементов, сгруппированных my_zip().
Примечание: следует учесть, что получаемые от пользователя списки могут быть разной длины. Как и встроенная zip(), my_zip() должна ограничивать размер возвращаемого списка длиной более короткого набора данных.
Пример ввода:
5 8 9 8 3 12 3 5 5 4 0 9 6 1 23 6 12 30
4 5 6 2 3 2 5 9 4 12 9 3
Вывод:
(5, 4) (8, 5) (9, 6) (8, 2) (3, 3) (12, 2) (3, 5) (5, 9) (5, 4) (4, 12) (0, 9) (9, 3)
54 85 96 82 33 122 35 59 54 412 09 93
9 13 15 10 6 14 8 14 9 16 9 12
Решение:
def my_zip(lst1, lst2):
result = []
for i in range(min(len(lst1), len(lst2))):
result.append((lst1[i], lst2[i]))
return result
def concatenation(lst1, lst2):
result = []
for i, j in my_zip(lst1, lst2):
result.append(str(i) + str(j))
return result
def add(lst1, lst2):
result = []
for i, j in my_zip(lst1, lst2):
result.append(i + j)
return result
sp1 = list(map(int, input().split()))
sp2 = list(map(int, input().split()))
def higher_order(function):
return function(sp1, sp2)
print(*higher_order(my_zip))
print(*higher_order(concatenation))
print(*higher_order(add))
Задание 7
Напишите программу, которая:
- получает от пользователя список слов и букву на отдельных строках;
- с помощью самописной функции my_filter() определяет, какие слова начинаются с полученной буквы;
- выводит индексы и слова отфильтрованного списка с помощью самописной функции my_enumerate().
Пример ввода:
абрикос бюро газета банк коробка стол бобр ноутбук блокнот баланс абажур
б
Вывод:
1-e слово нового списка - бюро
2-e слово нового списка - банк
3-e слово нового списка - бобр
4-e слово нового списка - блокнот
5-e слово нового списка – баланс
Решение:
def my_enumerate(lst, start=0):
for i in lst:
yield (start, i)
start += 1
def my_filter(function, items):
result = []
for item in items:
if function(item):
result.append(item)
return result
my_list = input().split()
letter = input()
for i, word in my_enumerate(my_filter(lambda x: x[0] == letter, my_list)):
print(f'{i + 1}-e слово нового списка - {word}')
Задание 8
Напишите функцию высшего порядка, которая принимает две одноаргументные функции первого порядка и возвращает новую функцию. Эта функция принимает аргумент x и применяет к нему полученные функции в следующем порядке:
function1(function2(x))
К примеру, если передать в функцию высшего порядка эти функции:
def add(x):
return x + 10
def multiply(x):
return x * 5
И вызвать функцию так:
print(super_function(add, float)('16'))
print(super_function(tuple, multiply)((3, 4, 5)))
print(super_function(str, multiply)('55'))
print(super_function(list, multiply)((1, 2, 3)))
Результат будет выглядеть следующим образом:
26.0
(3, 4, 5, 3, 4, 5, 3, 4, 5, 3, 4, 5, 3, 4, 5)
5555555555
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]
Решение 1:
def super_function(function1, function2):
def new(x):
return function1(function2(x))
return new
Решение 2:
def super_function(function1, function2):
return lambda x: function1(function2(x))
Задание 9
Напишите декоратор, который будет подсчитывать количество позиционных и именованных аргументов, переданных в функцию первого порядка.
Пример функции и вызова 1:
@decorator_func
def names_and_age(age1, age2, age3, name1, name2, name3):
return f'У меня есть три сестры: {name1}, ей {age1} лет; {name2}, ей {age2} лет; {name3} - ей {age3} лет\n'
print(names_and_age(12, 15, 13, name1='Света', name2='Маша', name3='Ира'))
Вывод 1:
Функция получила позиционных аргументов: 3, именованных аргументов: 3
У меня есть три сестры: Света, ей 12 лет; Маша, ей 15 лет; Ира - ей 13 лет
Пример функции и вызова 2:
@decorator_func
def position_and_salary(sal1, sal2, sal3, sal4, pos1, pos2, pos3, pos4):
return f'{pos1} получает {sal1} тыс, {pos2} получает {sal2} тыс, {pos3} получает {sal3} тыс, {pos4} получает {sal4} тыс\n'
print(position_and_salary(320, 150, 230, 170, pos1='разработчик', pos2='тестировщик', pos3='девопс', pos4='сисадмин'))
Вывод 2:
Функция получила позиционных аргументов: 4, именованных аргументов: 4
разработчик получает 320 тыс, тестировщик получает 150 тыс, девопс получает 230 тыс, сисадмин получает 170 тыс
Решение:
def decorator_func(decorated_func):
def wrapper_func(*args, **kwargs):
print(f'Функция получила позиционных аргументов: {len(args)}, именованных аргументов: {len(kwargs)}')
return decorated_func(*args, **kwargs)
return wrapper_func
Задание 10
Напишите декоратор, который будет измерять производительность функций, создающих список с помощью этих методов:
- range()
- списковое включение
- append()
- конкатенация
Среди показателей должны быть:
- Время работы функции.
- Текущее потребление памяти.
- Пиковое потребление памяти.
Пример вызова:
print(make_list_with_range())
print(make_list_comprehension())
print(make_list_with_append())
print(make_list_concatenation())
Вывод:
Название функции: make_list_with_range
Использованный метод: range()
Текущее потребление памяти: 0.290164 мб
Пик использования памяти: 2.289118 мб
Операция заняла: 0.112532 секунд
Функция make_list_with_range завершила работу
------------------------------------------------
Название функции: make_list_comprehension
Использованный метод: list comprehension
Текущее потребление памяти: 0.000930 мб
Пик использования памяти: 1.947573 мб
Операция заняла: 0.085460 секунд
Функция make_list_comprehension завершила работу
------------------------------------------------
Название функции: make_list_with_append
Использованный метод: append()
Текущее потребление памяти: 0.000582 мб
Пик использования памяти: 1.947229 мб
Операция заняла: 0.100597 секунд
Функция make_list_with_append завершила работу
------------------------------------------------
Решение:
import tracemalloc
from time import perf_counter
from functools import wraps
import inspect
def time_memory_used(function):
@wraps(function)
def wrapper(*args, **kwargs):
tracemalloc.start()
start = perf_counter()
result = function(*args, **kwargs)
current, peak = tracemalloc.get_traced_memory()
stop = perf_counter()
print(f'Название функции: {function.__name__}')
print(f'Использованный метод: {function.__doc__}')
print(f'Текущее потребление памяти: {current / 10**6:.6f} мб \n'
f'Пик использования памяти: {peak / 10**6:.6f} мб ')
print(f'Операция заняла: {stop - start:.6f} секунд')
tracemalloc.stop()
return result
return wrapper
@time_memory_used
def make_list_with_range():
'range()'
my_list = list(range(100000))
return f'Функция {inspect.stack()[0][3]} завершила работу \n{"-" * 48}'
@time_memory_used
def make_list_comprehension():
'list comprehension'
my_list = [l for l in range(100000)]
return f'Функция {inspect.stack()[0][3]} завершила работу \n{"-" * 48}'
@time_memory_used
def make_list_with_append():
'append()'
my_list = []
for item in range(100000):
my_list.append(item)
return f'Функция {inspect.stack()[0][3]} завершила работу \n{"-" * 48}'
@time_memory_used
def make_list_concatenation():
'конкатенация'
my_list = []
for item in range(100000):
my_list = my_list + [item]
return f'Функция {inspect.stack()[0][3]} завершила работу \n{"-" * 48}'
print(make_list_with_range())
print(make_list_comprehension())
print(make_list_with_append())
print(make_list_concatenation())
Подведем итоги
Функции высшего порядка помогают производить сложные вычисления, и делают код максимально абстрактным и компактным. Кроме того, с помощью функций высшего порядка реализуют декораторы, которые широко используются при разработке, и позволяют не писать с нуля часть функциональности, необходимой для работы проекта.
В следующей статье будем работать с файлами и файловой системой.