“Mastering the Power of Small Functions in Python: A Guide to Lambdas and Functors”
Are you tired of writing verbose code that sacrifices readability and maintainability? Say hello to functors in Python! Originally a concept from functional programming languages, functors can be a game-changer in object-oriented programming as well. By encapsulating data and behavior into a single object, functors can maintain state and be passed around as objects, making your code more flexible and concise. And with the power of lambdas, you can implement functors in Python with just a few lines of code. So let’s dive in and learn how to make your code more fun with functors!
Whereas functors have primarily been a useful programming concept in functional programming languages like Haskell, Lisp, or Ocaml, the concept has been successfully applied to object-oriented languages like Java and Python. So what exactly are functors ? Generally, a functor is a class or an object that behaves like a function by encapsulating data and behavior into a single object. They have such additional properties as state or arguments with them and therefore can be used when we want to maintain state. In Python, any object that implements the __call__
method can be considered a functor. With this, objects like functions, methods, or even classes can be considered functors.
To explain functors, let's write a basic function that takes an integer and returns its square.
def square(x):
return x ** 2
This function takes an integer x
as input, calculates its square using the exponent operator **
, and returns the result.
Example usage is as shown below:
>>> square(3)
9
>>> square(5)
25
>>> square(-2)
4
In this case, for a simple function like this, there is no significant advantage to implementing it as a functor since we don’t need to encapsulate state with behavior. However, to represent this as a functor, we have :
class Square:
def __init__(self, x):
self.x = x
def __call__(self):
return self.x ** 2
This is a simple functor that takes an integer x
as input and stores it as an instance variable. The__call__
method calculates the square of the stored integer and returns the result. We can create an instance of this class and use it like a function:
>>> square_of_3 = Square(3)
>>> square_of_5 = Square(5)
>>> square_of_3()
9
>>> square_of_5()
25
In this example, the Square
functor stores the integer x
as an instance variable, which is available every time the functor is called. This can be useful in cases where the state needs to be maintained across multiple calls to the function. Another advantage of functors is that they can be passed around as objects, just like any other Python object. This makes them more flexible than simple functions, which cannot be passed around as objects.
Alternatively, we can do it this way:
class Square:
def __init__(self):
self.x = None
def __call__(self, x):
self.x = x
return self.x ** 2
In this implementation, Square
is a class that has an __init__
method that initializes an instance variable self.x
to None
. The __call__
method takes an integer x
as input, stores it in self.x
, calculates the square of self.x
using the exponent operator **
, and returns the result.
We can create an instance of this class and use it like a function:
square = Square()
print(square(2)) # Output: 4
print(square(3)) # Output: 9
print(square(4)) # Output: 16
In this example, the Square
functor maintains the state of self.x
across multiple calls to the function. Each time we call square(x)
, the value of x
is stored in self.x
, and the square of self.x
is returned. Using a functor instead of a simple function allows us to encapsulate the state (in this case, the value of x
) within the object itself. We can also create multiple instances of the Square
functor, each with its own state.
What if we are looking for cleaner code that is just a stateful function without the hassle of defining a class or a named function? Cue lambdas.
A lambda defines a small, anonymous function, and, unlike a named function, lambdas are nameless and are defined inline in the code with only one expression. Its syntax takes the form of using the keyword lambda followed by parameters enclosed between parentheses (), then followed by a colon (:), and finally ending with an expression following the return statement syntax. They can be useful when you need a simple function that you don’t want to define explicitly with a def
statement.
In the case of the Square
example, we can define a lambda that squares an integer like this:
square = lambda x: x ** 2
This lambda takes an integer x
as input, calculates its square using the exponent operator **
, and returns the result. Look at that beautiful one liner!!!
Its output looks like this :
print(square(2)) # Output: 4
print(square(3)) # Output: 9
print(square(4)) # Output: 16
However, since we were talking about functors, lets represent this example as such.
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
This lambda function takes an argument x
and an optional argument f
that defaults to a lambda that returns x
. When called with x
, it checks if f(x) == x
. If so, it returns the square of x
using f()**2
. If not, it recursively calls itself with x
and a new lambda that returns x
.
If you need to visualize this, Philip Guo’s PythonTutor comes in handy and is free to use. This image below is a visualization of the same courtesy of Cornell University’s CS1110 version of python tutor by the legendary Prof. Walker White.
In general, using a lambda to implement a functor will not necessarily provide any significant efficiency gains over using a regular named function. The primary advantage of using a lambda is that it allows you to define a function inline without having to give it a name, which can be useful in certain situations.
However, in some cases, using a lambda may be more efficient than defining a regular named function, especially when the function is very simple and only needs to be used in one place in the code. In these cases, the overhead of defining a separate function may be eliminated by using a lambda instead.
That being said, in most cases, the difference in efficiency between using a lambda and using a regular named function will be negligible, and you should choose the option that makes the code most readable and maintainable.
I’d be remiss not to mention big O haha. Disappointedly, there isn’t much to analyze here. The big O analysis of the above code depends on the complexity of the underlying function being implemented as a functor or lambda.
For example, if the function being implemented has a time complexity of O(1) for each call, then the time complexity of the functor or lambda will also be O(1). In this case, the time complexity will not be affected by whether the function is implemented as a functor or lambda.
On the other hand, if the function being implemented has a time complexity of O(n) for each call, then the time complexity of the functor or lambda will also be O(n). In this case, the time complexity will again not be affected by whether the function is implemented as a functor or lambda.
In general, the time complexity of a functor or lambda will depend on the time complexity of the underlying function being implemented, as well as any additional operations or iterations performed within the functor or lambda itself. Therefore, it is important to consider the time complexity of the entire code segment that uses the functor or lambda, rather than just the functor or lambda itself.
In conclusion, functors can be a powerful tool in Python for encapsulating state and behavior into a single object, making code more concise and flexible. Any object that implements the call method can be considered a functor, and they can be passed around as objects just like any other Python object. In cases where a named function or a class feels like overkill, lambdas can be used to define small, anonymous functions inline in code. Overall, using functors and lambdas can make Python code more readable, maintainable, and concise, while also providing flexibility and power in dealing with stateful computations.
Sources:
6. Expressions. (n.d.). Python Documentation. https://docs.python.org/3/reference/expressions.html
3. Data model. (n.d.). Python Documentation. https://docs.python.org/3/reference/datamodel.html
A Guide to Python Lambda Functions, with Examples — SitePoint. (n.d.). A Guide to Python Lambda Functions, With Examples — SitePoint. https://www.sitepoint.com/python-lambda-functions/