Вы устали писать многословный код, жертвуя читабельностью и ремонтопригодностью? Поздоровайтесь с функторами в Python! Первоначально концепция функциональных языков программирования, функторы также могут изменить правила игры в объектно-ориентированном программировании. Инкапсулируя данные и поведение в один объект, функторы могут поддерживать состояние и передаваться как объекты, что делает ваш код более гибким и лаконичным. А с помощью лямбда-выражений вы можете реализовать функторы на Python всего несколькими строками кода. Итак, давайте погрузимся и узнаем, как сделать ваш код более интересным с помощью функторов!

В то время как функторы в первую очередь были полезной концепцией программирования в функциональных языках программирования, таких как Haskell, Lisp или Ocaml, эта концепция была успешно применена к объектно-ориентированным языкам, таким как Java и Python. Так что же такое функторы? Как правило, функтор — это класс или объект, который ведет себя как функция, инкапсулируя данные и поведение в один объект. У них есть такие дополнительные свойства, как состояние или аргументы, и поэтому их можно использовать, когда мы хотим поддерживать состояние. В Python любой объект, реализующий метод __call__, может считаться функтором. При этом такие объекты, как функции, методы или даже классы, можно считать функторами.

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

def square(x):
    return x ** 2

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

Пример использования показан ниже:

>>> square(3)
9
>>> square(5)
25
>>> square(-2)
4

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

class Square:
    def __init__(self, x):
        self.x = x
    
    def __call__(self):
        return self.x ** 2

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

>>> square_of_3 = Square(3)
>>> square_of_5 = Square(5)
>>> square_of_3()
9
>>> square_of_5()
25

В этом примере функтор Square сохраняет целое число x в качестве переменной экземпляра, которая доступна каждый раз при вызове функтора. Это может быть полезно в тех случаях, когда необходимо поддерживать состояние при нескольких вызовах функции. Еще одним преимуществом функторов является то, что их можно передавать как объекты, как и любой другой объект Python. Это делает их более гибкими, чем простые функции, которые нельзя передавать как объекты.

В качестве альтернативы, мы можем сделать это следующим образом:

class Square:
    def __init__(self):
        self.x = None
    
    def __call__(self, x):
        self.x = x
        return self.x ** 2

В этой реализации Square — это класс с методом __init__, который инициализирует переменную экземпляра self.x значением None. Метод __call__ принимает целое число x в качестве входных данных, сохраняет его в self.x, вычисляет квадрат self.x с помощью оператора экспоненты ** и возвращает результат.

Мы можем создать экземпляр этого класса и использовать его как функцию:

square = Square()

print(square(2))   # Output: 4
print(square(3))   # Output: 9
print(square(4))   # Output: 16

В этом примере функтор Square поддерживает состояние self.x при нескольких вызовах функции. Каждый раз, когда мы вызываем square(x), значение x сохраняется в self.x, и возвращается квадрат self.x. Использование функтора вместо простой функции позволяет нам инкапсулировать состояние (в данном случае значение x) внутри самого объекта. Мы также можем создать несколько экземпляров функтора Square, каждый со своим собственным состоянием.

Что, если мы ищем более чистый код, который представляет собой просто функцию с сохранением состояния без хлопот с определением класса или именованной функции? Подсказка лямбда-выражения.

Лямбда-выражение определяет небольшую анонимную функцию, и, в отличие от именованной функции, лямбда-выражения безымянны и определяются в коде только одним выражением. Его синтаксис принимает форму использования ключевого слова лямбда, за которым следуют параметры, заключенные в круглые скобки (), затем следует двоеточие (:) и, наконец, заканчивается выражением, следующим за синтаксисом оператора return. Они могут быть полезны, когда вам нужна простая функция, которую вы не хотите явно определять с помощью оператора def.

В случае примера Square мы можем определить лямбду, которая возводит в квадрат целое число следующим образом:

square = lambda x: x ** 2

Эта лямбда принимает целое число x в качестве входных данных, вычисляет его квадрат с помощью оператора экспоненты ** и возвращает результат. Посмотрите на этот красивый лайнер!!!

Его вывод выглядит следующим образом:

print(square(2))   # Output: 4
print(square(3))   # Output: 9
print(square(4))   # Output: 16

Однако, поскольку мы говорили о функторах, давайте представим этот пример как таковой.

square = lambda x, f=lambda x: x: f(x)**2 if f(x) == x else square(x, lambda y: x)

#running outputs:
print(square(2))   # Output: 4
print(square(3))   # Output: 9
print(square(4))   # Output: 16

Эта лямбда-функция принимает аргумент x и необязательный аргумент f, который по умолчанию равен лямбда-выражению, возвращающему x. При вызове с x проверяется наличие f(x) == x. Если это так, он возвращает квадрат x, используя f()**2. Если нет, он рекурсивно вызывает себя с x и новой лямбдой, которая возвращает x.

Если вам нужно визуализировать это, PythonTutor от Philip Guo пригодится и может использоваться бесплатно. Это изображение ниже является визуализацией того же любезно предоставленного Корнельского университета версии CS1110 репетитора по python легендарного проф. Уокер Уайт.

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

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

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

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

Например, если реализуемая функция имеет временную сложность O(1) для каждого вызова, то временная сложность функтора или лямбды также будет равна O(1). В этом случае на временную сложность не будет влиять то, реализована ли функция как функтор или лямбда.

С другой стороны, если реализуемая функция имеет временную сложность O(n) для каждого вызова, то временная сложность функтора или лямбды также будет равна O(n). В этом случае временная сложность снова не будет зависеть от того, реализована ли функция как функтор или лямбда.

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

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

Источники:

6. Выражения. (н.д.). Документация по Питону. https://docs.python.org/3/reference/expressions.html

3. Модель данных. (н.д.). Документация по Питону. https://docs.python.org/3/reference/datamodel.html

Руководство по лямбда-функциям Python с примерами — SitePoint. (н.д.). Руководство по лямбда-функциям Python с примерами — SitePoint. https://www.sitepoint.com/python-lambda-functions/