Функторы в Python Python

Функторы в Python

Функциональные объекты, или функторы в программировании — объекты, которые можно вызывать, подобно функциям. В этой статье мы разберемся, как создавать функторы и какие возможности они открывают.

Как создать функтор?

Как известно, объекты в Python являются экземплярами класса. Значит, для создания функторов нам тоже нужен класс. Однако обычно объекты нельзя вызвать.

a = 1
# TypeError: 'int' object is not callable

Для того чтобы расширить функциональность объекта, нам понадобится переопределить магический метод __call__. Он отвечает за поведение объекта при вызове.

class Functor:
    def __call__(self, *args):
        print('Вызов функтора с аргументами:', *args)
functor = Functor()
functor(1, 'q')
# Вызов функтора с аргументами: 1 q

При вызове наш функтор выполняет ровно то, что мы записали в метод __call__. Но что толку от функтора, если он заменяется простой функцией? Ведь нам доступны все возможности, которые можно реализовать с помощью класса!

Функтор, как замена замыкания и частичного применения функции

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

class SetNames:
    def __init__(self, names):
        self.names = []
        self.results = dict()
        self.add(names)
    def __call__(self, surname):
        result = self.results.get(surname)
        if self.results == dict():
            self.results.update({surname: []})
            for name in self.names:
                self.results[surname].append(
                    f'{name} {surname}'
                )
            result = self.results[surname]
        return result
    def add(self, lst: list):
        for i in lst:
            if not isinstance(i, str):
                raise TypeError(
                    'Имена должны быть строками'
                )
        self.results.clear()
        self.names.extend(lst)
girl_names = SetNames(('Julia', 'Lisa'))
girl_names.add(['Inna', 'Anna'])
print(girl_names('Koval'))
# ['Julia Koval', 'Lisa Koval', 'Inna Koval', 'Anna Koval']

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

Классы-декораторы

Поскольку функторы можно вызывать, само собой напрашивается использование их классов в роли декораторов.

from datetime import datetime


class Validation:
    def __init__(self, function):
        self.function = function

    def __call__(self, *args, **kwargs):
        res = self.function(*args, **kwargs)
        if not 0 < res < 100:
            raise ValueError(
                'Validation failed'
            )
        return f'{datetime.now()}: {res}'


@Validation
def square(a):
    return a**2

print(square(8))
# 2022-10-29 09:24:29.503697: 64
print(square(12))
# ValueError: Validation failed

Конструктор нашего класса-декоратора принимает только один аргумент — функцию. В методе __call__ мы проверяем, находится ли возвращаемое значение в диапазоне от 0 до 100. Если условие не выполняется, мы вызываем ошибку, а иначе просто возвращаем строку с текущим временем и полученным числом. Но можно пойти дальше, и сделать декоратором объект класса.

class Validation:
    def __init__(self, lower, upper):
        self._lower = lower
        self._upper = upper

    def __call__(self, arg):
        def wrapper(a):
            res = arg(a)
            if not self._lower < res < self._upper:
                raise ValueError(
                    'Validation failed'
                )
            return f'{datetime.now()}: {res}'
        return wrapper

@Validation(0, 10)
def square(a):
    return a**2


print(square(2))
# 2022-10-29 09:41:59.377104: 4
print(square(4))
# ValueError: Validation failed

Теперь мы можем передавать границы интервала напрямую в декоратор.

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

Заключение

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

Практический Python для начинающих
Практический Python для начинающих

Станьте junior Python программистом за 7 месяцев

 7 месяцев

Возможно будет интересно

Как практиковаться в Python? Python
Новичок
Как практиковаться в Python?

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

2022-10-19
match-case: сопоставление структурных шаблонов Python
Продвинутый
match-case: сопоставление структурных шаблонов

Сопоставление структурных шаблонов с помощью match-case во многих случаях проще и предпочтительнее использования условных конструкций. Давайте разберемся, как с ним работать.

2022-10-28
Шпаргалка по модулю itertools Шпаргалки
Продвинутый
Шпаргалка по модулю itertools

Шпаргалка по всем функциям модуля itertools, создающим разнообразные итераторы.

2022-10-28