Callable Objects
Callable Object: Functor¶
In Python the most common type of functor is the Function. There are many other types of functors in Python: Lambda, Method, Module, Package and Class.
It may seem odd to see module and package on that list. We will come back to this idea in the Module section. Here we will focus on the three functors that typically come to mind first when thinking of callable objects:
- Function
- Lambda
- Method
Function¶
def is_even(number: int) -> bool:
""" This is a simple function to test if an integer is even or odd.
@param number int :: input number to be tested.
@return bool :: True for even numbers, False for odd numebrs. """
return number % 2 == 0
Functions are the most common type of callable object in Python, perhaps in all of programming.
The function above has several parts:
def
This is the function definition keyword. Everything indented under it is considered inside that function. This is function scope. Everything in the function scope must be defined before it is used. This is the closest thing to block scope that we have in Python.is_even()
This is the name of the function. This is used to invoke the function later, this is also known as calling the function.number
This is the parameter named “number”, it asks for anint
type input argument.: int
This is a type hint, they come after a colon after an argument. Type hints are not enforced by Python, and they are relatively new to the language. Type hints are always optional and rarely used.-> bool
This is the return type hint, it is also optional.""" ... """
This is the doc string. It can be used to document your work. Proper doc strings are defined at the top of a function, otherwise it is just a comment.return ...
This is the return statement of the function. Python supports the use of multiple returns in a single function. This is frowned upon in some circles.
Lets test our function in a loop:
for num in range(10):
print(num, is_even(num))
Next, lets look at the documentation we made in the doc string.
help(is_even)
The is_even()
function above takes only one positional argument. Functions, lambdas and methods can be defined in such a way to take an arbitrary number of arguments with the *args
parameter. This is related to the **kwargs
parameter. They are often seen together but they do not depend on one another. Each of them are capable of allowing you to define functions that take an arbitary number of arguments. This difference is that args
is more like a list of arguments and kwargs
is more like a dictionary of named arguments.
def print_all(*args):
for itm in args:
print(itm)
print_all("One to Five:", 1, 2, 3, 4, 5)
We can combine these ideas with the map()
function.
def is_even_map(*args):
return list(map(is_even, args))
print(is_even_map(1, 2, 3, 4, 5, 6))
Lambda¶
Lambdas are very similar to anonymous functions in other languages. They’re defined in-line and they’re considered beautiful when done properly. Lambdas have one clear advantage over other types of callable objects: they’re very terse. They also have one subtle advantage: they can be defined in places where function cannot. However, no matter what you’re doing, if a lambda can do it, then there’s also a way to use a proper function or method instead.
Generally speaking if your lambda is over 30 characters, you’re doing it wrong. This is not a hard limit, however you only get one line to define a lambda, and they often don’t begin at the beginning of a line, do the math.
A wise hacker once said…
lambda(lambda) -> lambda
always solves everything – for arbitrary definitions of “always”, “solves” and “everything”.
Let that sink in. It’s called Lambda Calculus, and unfortunately – it’s totally beyond the scope of this teaching guide. Lambda Calculus is one of the most fundamental ideas in programming, please go check it out. It will blow your mind… in a good way.
# Named Lambda
callable_lambda = lambda x: 2**x
print(callable_lambda(8))
This same thing can be done in one line (see below), it resembles what in JavaScript is known as an IIFE or Immediately Invoked Function Expression. Notice that the lambda suddenly requires a pair of enclosing parens. This is to disambiguate the calling perens from the lambda itself.
# Immediately Invoked Annonymous Lambda
# print(lambda x: 2**x(8)) # This doesn't work
print((lambda x: 2**x)(8))
Method¶
Methods are similar to functions, but they live inside classes. While this is true, it kinda misses the point. Methods will be discussed in more detail in the Classes Module.
Here’s a simple example of a classmethod featuring the builtin sum()
function and the randint()
function from the random module.
import random
class Wizard:
hit_dice = 6
level = 10
@classmethod
def total_health(cls):
return sum(random.randint(1, cls.hit_dice) for _ in range(cls.level))
print(Wizard.total_health())
Here’s a simple example of an instance method…
class Fighter:
damage = 6
attacks = 4
def calculate_damage(self):
return sum(random.randint(1, self.damage) for _ in range(self.attacks))
fighter = Fighter()
print(fighter.calculate_damage())
The above example is contrived to illustrate two types of methods. This is not intended to be a good example of how to write classes. The Fighter and Wizard classes should be polymorphic and these are not. More information on polymorphism and other OOP concepts will be found in the Classes Module.
The ID Functor, in Three Forms…¶
The ID Function is really simple, it might be the most simple function that does something, because it nearly does nothing. It takes one argument and returns it. While this function may seem useless, it really isn’t (see lambda calculus). However, it’s only used here as a simple example to compare the syntax of the three functor types.
The choice between creating a function, lambda or method is largely one of style. Your team may establish norms and you should follow them, project consistancy is paramount. When in doubt, choose the function style, it will never let you down.
ID Function:
def id_func(obj):
return obj
print(id_func(42))
ID Lambda:
id = lambda obj: obj
print(id(42))
For the lambda expression itself, you can’t use any annotations (the syntax on which Python’s type hinting is built). The syntax is only available for def function statements. ~StackOverflow
ID Method:
In C++ this method is known as the call operator. If this method is defined on a Python class, the resulting instance(s) will be callable.
class Identity:
""" Produces a callable instance. """
def __call__(self, obj):
""" Models the identity function. """
return obj
id_object = Identity()
print(id_object(42))
This is wrong:
Identity.__call__() # Magic Methods are never called by name
Prefered way to invoke a callable instance:
obj = Identity() # instantiation
obj() # invocation
Curious Alternative:
Identity()() # both at once
It should be noted that all three of the id functors above are also higher-order functors. Which leads us to the next section of this module…
Higher Order Functor¶
Unlike first-order functors, the higher-order functor is a functor that takes another functor as input, or returns an uncalled functor as output. Functions, lambdas and methods are all first-class objects in Python, so if you want to pass a functor to another functor, you can treat it as any other object.
For this example we will look at apply()
, a functor that takes a functor and an arbitrary number of additional arguments as input. This functor will call the input functor with the other arguments as its input, and then it will return the result.
def apply(func, *args, **kwargs):
return func(*args, **kwargs)
print(apply(id, 42))
The opposite of apply()
might be bind()
. Bind takes a functor and its arguments as input, then returns a callable rather than returning the result of calling it. The functor is wrapped in a lambda, composed with its arguments and made ready to be called later with no additional inputs.
def bind(func, *args, **kwargs):
return lambda: func(*args, **kwargs)
bound_id = bind(id, 42)
Don’t forget to call the bound functor…
print(bound_id) # not called
print(bound_id()) # called