Objects

First-Class Functions

Functions are first-class citizens in Python. This means that functions can be assigned to variables and passed around just like every other object in Python.

>>> def square(x):
...     return x ** 2
...
>>> def cube(x):
...     return x ** 3
...
>>> operations = [square, cube]

We just put some functions inside a list.

>>> operations
[<function square at 0x7fc8abd2ec80>, <function cube at 0x7fc8abcd7158>]

We can call these functions from inside the list the same we call them from outside.

>>> square(2)
4
>>> cube(2)
8
>>> operations[0](2)
4
>>> operations[1](2)
8

We can even conditionally call one function or the other:

>>> numbers = range(1, 10)
>>> [operations[i % 2](n) for i, n in enumerate(numbers)]
[1, 8, 9, 64, 25, 216, 49, 512, 81]

We have already seen that you can pass functions into other functions. The map and filter functions we learned about last time accept a function as their first argument.

>>> list(map(bool, ["word", "", 5, 0]))
[True, False, True, False]
>>> list(filter(bool, ["word", "", 5, 0]))
['word', 5]

The sorted function sorts a given iterable. You can optionally pass a function to it to generate keys for use in sorting.

>>> fruits = ["lemon", "apple", "banana", "lime"]
>>> sorted(fruits)
['apple', 'banana', 'lemon', 'lime']
>>> sorted(fruits, key=len)
['lime', 'lemon', 'apple', 'banana']

Here we’ve sorted fruits by their string length. We could also pass in a function that will create a key consisting of a tuple with the length of the string and the string itself. This would sort the words by their length and then by their usual alphabetical order.

>>> sorted(fruits, key=lambda w: (len(w), w))
['lime', 'apple', 'lemon', 'banana']

This works because when we sort tuples, they are sorted based on each of their elements in order:

>>> sorted([(5, "lemon"), (5, "apple"), (4, "lime")])
[(4, 'lime'), (5, 'apple'), (5, 'lemon')]
>>> (4, "lime") < (5, "apple")
True
>>> (5, "lemon") < (5, "apple")
False

Let’s make our own function that accepts a function as an argument. For fun we’ll implement a map_and_filter function that does a filter followed by a map:

>>> def map_and_filter(mapper, predicate, iterable):
...     return list(map(mapper, filter(predicate, iterable)))
...
>>> map_and_filter(len, bool, ["hello", "", "world!"])
[5, 6]

It might be more clear to implement this using a list comprehension instead:

>>> def map_and_filter(mapper, predicate, iterable):
...     return [mapper(x) for x in iterable if predicate(x)]
...
>>> map_and_filter(len, bool, ["hello", "", "world!"])
[5, 6]

We can also define new functions inside of functions, and functions can also be return values. Here’s a somewhat silly example:

>>> def make_adder(x):
...     def add(y):
...         return x + y
...     return add
...
>>> add4 = make_adder(4)
>>> add4(5)
9
>>> add4(3)
7

Let’s make a function that allows us to pre-load a function call with some arguments.

>>> def partial(func, *first_args):
...     def new_func(*args):
...         new_args = first_args + args
...         return func(*new_args)
...     return new_func
...
>>> count_lengths = partial(map, len)
>>> word_lengths = count_lengths(["some", "words", "banana"])
>>> word_lengths
<map object at 0x7f0a688cf128>
>>> list(word_lengths)
[4, 5, 6]

Python actually already has a function like this in the standard library. It’s the partial function in the functools module. Take a look at the help for partial.

>>> from functools import partial
>>> help(partial)

Functions that accept other functions as arguments or return functions are called “higher order functions”. We’ll look more into ways we can use higher order functions in later workshops.

Review of Lambdas

Lambda functions are anonymous functions, which means they don’t have a name and they can be passed around as arguments at their creation time. They are basically just shorthand for a function containing a single expression that acts on its argument list. The result of the lambda expression is a function object.

Regular functions need to be defined and then used after they are defined. Lambda functions can be defined and then called or passed around at the same time.

Classes are Objects

Let’s make a class that doesn’t really do anything:

>>> class A: pass
...

pass is a “no-op” statement meaning it doesn’t actually perform an operation. It does nothing; it is just a placeholder.

We’re able to write everything on one line instead of using a block syntax. You’ll typically never see this in code because it’s bad form.

Let’s make an instance of our class:

>>> a = A()

Let’s try to call the greet method on our class:

>>> a.greet()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'A' object has no attribute 'greet'

That didn’t work because we never made a greet method on our class!

Let’s add one now. First we’ll make a function and then we’ll add an attribute on our class that contains the function.

>>> def greet(self):
...     print("Hello!!")
...
>>> A.greet = greet

Now when we call the greet method on our class instance, what will happen?

>>> a.greet()
Hello!!

It works! We modified our class after creating it. This is sometimes called monkey patching and you’ll sometimes see this kind of dynamic behavior used in automated code tests.

Now let’s undo our modification by deleting the greet attribute from our object:

>>> del A.greet

Now if we try to call greet again we’ll see that we once again get an error because there is no longer a greet method on our class.

>>> a.greet()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'A' object has no attribute 'greet'

This isn’t something we would normally do to a class. We only did this to demonstrate that classes in Python are just another kind of object.

Functions are Objects

Functions are objects too. For example functions have attributes.

The __doc__ attribute lists the documentation string for a function. Here is the documentation string for the len function:

>>> len.__doc__
'Return the number of items in a container.'
>>> print(len.__doc__)
Return the number of items in a container.
>>> help(len)

We can also set our own attributes on functions.

>>> def greet():
...     print("hello world")
...
>>> greet.some_attribute = 4
>>> greet.some_attribute
4
>>> greet()
hello world

Sometimes this might change behavior. For example:

>>> greet
<function greet at 0x7f0a688cb950>
>>> greet.__qualname__
'greet'
>>> greet.__qualname__ = "say hello"
>>> greet
<function say hello at 0x7f0a688cb950>

In Python, everything is an object.

Object Exercises

Inverse Filter

This is the exclude exercise in objects.py.

Create an exclude function that only keeps items which fail a given predicate test. The function should accept a function and an iterable as its arguments and should return an iterable containing all items which yielded a falsey return value from the predicate function. This is basically the opposite of the built-in filter function.

>>> exclude(bool, [False, True, False])
[False, False]
>>> exclude(lambda x: len(x) > 3, ["red", "blue", "green"])
['red']

Call

This is the call exercise in objects.py.

Write a function call which calls a given function with any given positional and keyword arguments and returns the value returned by the function call.

>>> call(int)
0
>>> call(int, "5")
5
>>> call(len, "hello")
5
>>> list(call(zip, [1, 2], [3, 4]))
[(1, 3), (2, 4)]

Call Later

This is the call_later exercises in objects.py.

Write a function call_later which accepts a function and a list of arguments and returns a new function that, when called, will call the function with the given arguments.

>>> names = []
>>> append_name = call_later(names.append, "Trey")
>>> append_name()
>>> names
['Trey']
>>> append_name()
>>> names
['Trey', 'Trey']
>>> call_zip = call_later(zip, [1, 2], [3, 4])
>>> list(call_zip())
[(1, 3), (2, 4)]

Call Again

This is the call_again exercise in objects.py.

Write a function call_again which accepts a function and a list of arguments and returns a tuple. The first item in the tuple should be the return value from calling the given function with the given arguments. The second item in the tuple should be a function that, when called, will call the function again with the given arguments.

>>> names = []
>>> response, names_as_str = call_again(str, names)
>>> response
'[]'
>>> names.append("Diane")
>>> names_as_str()
"['Diane']"

Only Once

This is the only_once exercise in objects.py.

Make a function only_once that accepts a function as an argument and returns a new function. The returned function should be identical to the original function except that when you try to call the function more than once, it shouldn’t let you.

>>> def do_something(x, y):
...     print(f"doing something with {x} and {y}")
...     return x * 2 + y ** 2
...
>>> do_something_once = only_once(do_something)
>>> do_something_once(1, 2)
doing something with 1 and 2
6
>>> do_something_once(1, 2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in new_func
ValueError: You can't call this function twice!

Cache

This is the cache exercise in objects.py.

Write a cache function which takes a function as its argument and returns a function that is identical to the original function except that it caches its return values based on any positional arguments given.

>>> def do_something(x, y):
...     print(f"doing something with {x} and {y}")
...     return x * 2 + y ** 2
...
>>> do_something_cached = cache(do_something)
>>> do_something_cached(1, 2)
doing something with 1 and 2
6
>>> do_something_cached(1, 2)
6

Partial

This is the partial exercise in objects.py.

Make a partial function (like the one we already made) which accepts positional arguments and keyword arguments.