Наталья Кайда 30 января 2023

🐍 Самоучитель по Python для начинающих. Часть 14: Функции высшего порядка, замыкания и декораторы

Разберем важные концепции, связанные с функциями высшего порядка, напишем собственные версии map(), reduce() и filter(), потренируемся в создании декораторов и решим 10 практических заданий.
🐍 Самоучитель по Python для начинающих. Часть 14: Функции высшего порядка, замыкания и декораторы

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

В программировании (и в математике) функциями высшего порядка называются функции, которые выполняют одно (или оба) из этих действий:

  1. Принимают одну (и более) функций в качестве аргументов.
  2. Возвращают функцию в качестве результата.

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

  1. Функцию первого порядка для проведения операции умножения или сложения.
  2. Список чисел от пользователя.
  3. Начальное значение – 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() для параллельной итерации двух наборов данных. Напишите функцию высшего порядка, которая возвращает функции для:

  1. Группировки параллельных элементов списков с помощью самописной my_zip().
  2. Конкатенации элементов, сгрупированных my_zip().
  3. Сложения элементов, сгруппированных 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()
  • конкатенация

Среди показателей должны быть:

  1. Время работы функции.
  2. Текущее потребление памяти.
  3. Пиковое потребление памяти.

Пример вызова:

        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())

    

Подведем итоги

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

В следующей статье будем работать с файлами и файловой системой.

***

Содержание самоучителя

  1. Особенности, сферы применения, установка, онлайн IDE
  2. Все, что нужно для изучения Python с нуля – книги, сайты, каналы и курсы
  3. Типы данных: преобразование и базовые операции
  4. Методы работы со строками
  5. Методы работы со списками и списковыми включениями
  6. Методы работы со словарями и генераторами словарей
  7. Методы работы с кортежами
  8. Методы работы со множествами
  9. Особенности цикла for
  10. Условный цикл while
  11. Функции с позиционными и именованными аргументами
  12. Анонимные функции
  13. Рекурсивные функции
  14. Функции высшего порядка, замыкания и декораторы
  15. Методы работы с файлами и файловой системой
  16. Регулярные выражения
  17. Основы скрапинга и парсинга
  18. Основы ООП: инкапсуляция и наследование
  19. Основы ООП – абстракция и полиморфизм
  20. Графический интерфейс на Tkinter
  21. Основы разработки игр на Pygame
  22. Основы работы с SQLite
  23. Основы веб-разработки на Flask
  24. Основы работы с NumPy
  25. Основы анализа данных с Pandas
***
Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека питониста»

МЕРОПРИЯТИЯ

Комментарии

ВАКАНСИИ

Добавить вакансию
PHP Developer
от 200000 RUB до 270000 RUB
Аналитик данных
Екатеринбург, по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ