Callable Objects

08_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

In [0]:
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 an int 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:

In [2]:
for num in range(10):
    print(num, is_even(num))
0 True
1 False
2 True
3 False
4 True
5 False
6 True
7 False
8 True
9 False

Next, lets look at the documentation we made in the doc string.

In [3]:
help(is_even)
Help on function is_even in module __main__:

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.

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.

In [0]:
def print_all(*args):
    for itm in args:
        print(itm)
In [5]:
print_all("One to Five:", 1, 2, 3, 4, 5)
One to Five:
1
2
3
4
5

We can combine these ideas with the map() function.

Map Function | python.org

In [0]:
def is_even_map(*args):
    return list(map(is_even, args))
In [7]:
print(is_even_map(1, 2, 3, 4, 5, 6))
[False, True, False, True, False, True]

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.

In [8]:
# Named Lambda
callable_lambda = lambda x: 2**x
print(callable_lambda(8))
256

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.

In [9]:
# Immediately Invoked Annonymous Lambda
# print(lambda x: 2**x(8))  # This doesn't work
print((lambda x: 2**x)(8))
256

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.

Sum Function | python.org

Random Module | python.org

In [0]:
import random
In [0]:
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))
In [12]:
print(Wizard.total_health())
27

Here’s a simple example of an instance method…

In [0]:
class Fighter:
    damage = 6
    attacks = 4

    def calculate_damage(self):
        return sum(random.randint(1, self.damage) for _ in range(self.attacks))
In [14]:
fighter = Fighter()
print(fighter.calculate_damage())
15

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:

In [0]:
def id_func(obj):
    return obj
In [16]:
print(id_func(42))
42

ID Lambda:

In [0]:
id = lambda obj: obj
In [18]:
print(id(42))
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

Lambda Type Hints | Stack Overflow

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.

In [0]:
class Identity:
    """ Produces a callable instance. """

    def __call__(self, obj):
        """ Models the identity function. """
        return obj
        
In [20]:
id_object = Identity()
print(id_object(42))
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.

In [0]:
def apply(func, *args, **kwargs):
    return func(*args, **kwargs)
In [22]:
print(apply(id, 42))
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.

In [0]:
def bind(func, *args, **kwargs):
    return lambda: func(*args, **kwargs)
In [0]:
bound_id = bind(id, 42)

Don’t forget to call the bound functor…

In [25]:
print(bound_id)  # not called
<function bind.<locals>.<lambda> at 0x7f7b701c6b70>
In [26]:
print(bound_id())  # called
42