Вы устали писать многословный код, жертвуя читабельностью и ремонтопригодностью? Поздоровайтесь с функторами в 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/