Today, I thought it would be fun to entertain a thought experiment for folks just learning how to code in Python: what if Python only had functions? The goal of this article is to show you that a lot can be accomplished using Python’s standard library without ever learning control flow.
Table of Contents
Say No to Control Flow (For Now)
In our previous article, we took some time to explore some common Python operators. In that process, I mentioned that an operator is a symbolic representation of a function. Of course, I had assumed that you were already familiar with this idea of a function based on our discussion around programming paradigms. However, we never actually got a chance to talk about Python functions.
Now, part of me wants to avoid this subject for now. After all, I mentioned that we were going to treat Python like it was an imperative language. And of course, we’re still going to do that! First, however, I want to take a detour to toy with the idea of Python as a purely functional language.
The purpose of this exercise isn’t to learn about functional programming. In fact, this exercise isn’t even meant to teach you how to create functions; we’ll do that later. Instead, the purpose of this article is to introduce a few common functions and how they can be used. That way, we can avoid the messiness of control flow for the time being while still doing some interesting stuff.
With that said, let’s go ahead and talk about functions!
Operators Are Simple Functions
Previously, we took some time to discuss the different classes of operators. For instance, we divided operators into two groups: unary and binary. These terms refer to the number of inputs an operator can process.
In the unary group, we have operators like the negative sign which can only accept a single input (i.e. the number to be negated). Likewise, in the binary group, we have operators like the addition sign which only works with two inputs (i.e. the two numbers to be added together).
After we discussed these groups, I mentioned that it was possible to have an operator with more than two inputs—though this rarely occurs. Instead, we usually opt for a function which is a black box that accepts a number of inputs and produces an output. Oddly enough, this is exactly what an operator does.
So, what makes an operator different from a function? Well for one, operators don’t have a standard syntax. For instance, unary operators usually appear to the left of their input while binary operators appear between their inputs (i.e. infix). What would a 3-nary or 4-nary operator look like?
On the flip side, functions have a standard syntax. In many languages, including Python, functions are called by a name followed by a set of parentheses:
Inside the parentheses, we can place any number of inputs as long as they’re separated by commas:
my_unary_function(10) my_binary_function(4, 5) my_trinary_function(1, 5, 3)
Naturally, this syntax scales indefinitely (for better or for worse). In fact, because this syntax scales so well, we can actually replace all of the operators with it:
add(3, 1) # returns 4 subtract(6, 11) # returns -5
Of course, we don’t have to implement these ourselves. In many cases, Python provides the functions we need right out of the box. In the next section, we’ll take a look at some of these functions.
Python’s Built-in Functions
In order for us to treat Python as a function-only language, we need to introduce a few functions. As I mentioned already, we’re not going to talk about how to make our own functions quite yet. Instead, we’re going to look at a handful of functions that are provided by the language.
Since we’re on the topic of operators, I figured what’s a better way of kicking off this list than with an overview of operator functions. As it turns out, all of the original operators are implemented as functions in the operator module.
To access the operator functions, we have to do something we haven’t discussed yet: module importing. Naturally, I don’t think it’s appropriate to talk about modules or packaging right now, so I’ll introduce you to the process:
from operator import *
By typing this line in Python, we load all of the operator functions into the environment. In other words, we gain access to the following functions:
- All mathematical functions (e.g. +, -, *, /)
- All relational functions (e.g. <, >, ==)
- Other stuff!
Now, instead of using the operators, we can use their functional equivalents. For example, instead of using the addition operator to add two numbers together, we can use the addition function:
add(5, 7) # returns 12
Naturally, we can replicate more complex expressions by nesting the functions:
sub(add(5, 7), 6) # returns 6
Of course, I think we can all agree that these sorts of expressions are easier to read in their operator form. For example, the code snippet above is equivalent to the following operator expression:
(5 + 7) - 6
This condensed notation is what makes operators so convenient. That said, there are plenty of other built-in functions that provide significantly more complex behavior.
In the previous section, we talked about all of the operator functions which are largely used for arithmetic and logic. As it turns out, there are several additional mathematical functions built into Python.
On top of being able to perform arithmetic, Python has functions available for computing other common mathematical operations. For example, perhaps we want to find the largest or smallest number in a list. Well, Python has the
min() functions, respectively.
These functions are a bit more flexible than our operator functions. Rather than strictly accepting two inputs, we can pass as many inputs as we’d like:
min(1, 5) # returns 1 min(4, 7, -3) # returns -3
At the risk of confusing you even further, these sort of functions also accept any “iterable” which is a fancy word for collection of items. It’s a bit more complicated than that, but the definition passes for our example below:
x = [1, 5, -3, 4, 2, 19] min(x) # returns -3
In addition to
min(), Python also includes functions for absolute value (
abs()) and sum (
sum()). In other words, we could write something as complex as the following:
min( sum([2, 5, abs(-4)]), max(13, 9), abs(sum([5, 3, 1])) ) # returns 9
If needed, we could start adding in our operator functions as well. That said, we’ll get to that later! For now, let’s talk a bit about iterable functions.
In the previous section, I mentioned this idea of an iterable which has a specific definition. However, for our purposes, think of an iterable as a collection of items like a list or a string.
In that case, Python has a few built-in functions for us. For instance, we already talked about
sum(), but are you familiar with
len()? In Python, we can get the length of most iterables using the
len([1, 2, 7]) # returns 3 len("Jeremy") # returns 6
len(), there are other cool iterable functions like
sorted() which we can use to sort most iterables:
sorted([5, -1, 3]) # returns [-1, 3, 5]
Now with these functions in our repertoire, we can create even more interesting code snippets entirely out of functions:
sorted([ len("Jeremy"), min(len("Grifski"), len("Renegade")), sum([4, 13, -3]) ]) # returns [6, 7, 14]
On a side note, this sort of code is starting to remind me of my article on obfuscation. In other words, this code isn’t exactly easy to read. That said, I think we’ve gained a considerable amount of power without ever introducing control flow, so that’s cool! Let’s take things a step further with higher-order functions.
Up to this point, we’ve really been limited by the available functions in Python. Sure, we can have a lot of fun with lists and mathematics, but we’re kind of treating Python like a glorified calculator. What gives us real power are higher-order functions.
In programming, a higher-order function is a function that accepts a function(s) as input. Although, functions are also considered higher-order if they return a function. In either case, functions become data.
Interestingly, we don’t have to do anything special to treat functions as data. In fact, all we have to do is remove the parentheses:
x = len # x now stores the len() function
Of course, this particular example isn’t all that interesting because we really only change the name of the function. In other words, we can call
len() using its new name
x = len x("Hello") # returns 5
That said, things get a lot more interesting when we consider higher-order functions. For example, there are two main built-in higher-order functions:
map() is a function that takes two inputs: a function and an iterable. The idea here is that we can take a function and apply it to every item in the iterable. The result is a new list composed of the changes:
names = ["Jeremy", "Morgan", "Robert", "Matt", "Seth"] map(len, names) # returns [6, 6, 6, 4, 4] as a map object
filter() is a function that does what its name implies: filters an iterable. Like
filter() also accepts a function and an iterable as input. In this case, the function will be used to determine which elements belong.
filter() to work, we need to provide a function that takes a single argument and returns a boolean: True of False. Given the functions we’ve explored already, this is kind of a tough ask. That said, there is one function from the operator module that we can take advantage of:
Remember a few articles back when I said that some values can evaluate to False in certain contexts? Well, we can take advantage of that here with
truth(). In particular, we can use it to filter out all the falsy values of a list:
x = [0, 5, 13, -7, 9] filter(truth, x) # returns [5, 13, -7, 9] as a filter object
When working with a list of numbers, this function would remove all zeroes.
As you can probably imagine, we can use both of these functions simultaneously. For example, we could first use
map() to convert our list of names to a list of lengths. Then, we could use
filter() to remove all the zeroes. If we’re feeling adventurous, we might even sort the results.
names = ["Jeremy", "", "Morgan", "Robert", "", "Matt", "Seth"] sorted(filter(truth, map(len, names))) # returns [4, 4, 6, 6, 6]
How cool is that? That’s a ton of computation done in just a few lines of code. Keep this sort of thing in mind as we continue our journey into imperative programming. It’ll save you a lot of time and effort. For now though, let me answer a few quick questions you might already have.
What About Methods?
If you have a bit of programming experience already, you might be familiar with this notion of methods which are functions that operate on objects (e.g. lists, strings, etc.). As I mentioned already, I plan to treat Python as an imperative language as long as possible, but this idea of object-oriented programming is going to come up again and again. After all, we’ve been using several built-in objects already.
For instance, we already know how to create a string:
x = "Hello"
Up to this point, we’ve been able to print and concatenate them as well as compute their length. That said, strings also have their methods. For example, we can convert a string to lowercase using the
x = "Hello" x.lower() # returns "hello"
This is clearly a bit different than our current understanding of functions. Up to this point, I mentioned that functions have a consistent syntax, and this syntax above breaks that rule. Instead of having a function name followed by arguments, we have some variable followed by a dot, a name, and some arguments. In other words, this isn’t a function at all; it’s a method.
Of course, the purpose of this article was to introduce you to the idea of functions as a stepping stone toward other imperative programming concepts. At some point, we will discuss methods, but today is not that day.
Why Restrict Ourselves to Functions?
When I was learning how to program, I was studying Java. If you’ve ever used Java, you know that it’s an extremely object-oriented language. Of course, it’s not purely object-oriented, but it’s largely designed that way.
One thing I really liked about that course was how well the instructor conveyed this intent in the language. In other words, we learned how to work with objects right away.
When I started teaching in grad school, our program didn’t treat Java as an object-oriented language. Instead, we treated it as an imperative language—much like how we’re treating Python in this early part of the series.
Now, there were definitely problems with this approach. For instance, it forced students to work around the object-oriented system to their own detriment. In fact, we often had to provide templates because students couldn’t write their own code without them.
Of course, I think the bigger issue was that sticking to imperative programming forced students to think about problems a certain way. As a result, students often missed the easy solution because their toolkit was too niche.
Now, of course, Python doesn’t really have the same problems that Java has. In fact, I’d argue that Python doesn’t really have a dominant paradigm. So, it’s possible to sort of pick one and stick with it.
That said, I think the big reason I put this article together was to quickly expose you to another way to solve problems before we go down the rabbit hole that is imperative programming.
See, the thing is that imperative programming begins to introduce complexity to our code, and it’s easy to get lost in the weeds. By introducing you to functions (and the basics of functional programming), I’m offering you a separate tool when you run into trouble. Keep that in mind!
Writing More Interesting Programs
With everything we’ve explored up to this point in the series, we’re fairly limited in the things we can do. There are basically four key pieces of information missing:
- Python Libraries
- Lambda Functions
- Function Definitions
- Imperative Programming
One way we can write more code based on what we already know is to begin looking through Python’s documentation. In other words, we can explore all the libraries available in Python to use as building blocks in our function-only programs.
Oddly enough, we could possibly skip the documentation exploration by instead writing our own functions. There are two main ways of doing this: lambda functions and function definitions. Being able to write our own functions drastically opens the door to new possibilities. For example, we could write our own filter functions.
Naturally, we will be exposed to all of these things as we go forward in this series. That said, for now we’re going to focus exclusively on the last option: imperative programming. This option allows us to begin applying some of the ideas we discussed surrounding algorithmic thinking. In other words, we can begin writing our own algorithms in a way that I think is intuitive to new learners.
Of course, that’s all I have for today. In the meantime, if you’re interested in supporting this series and watching it grow, I recommend checking out my list of ways to grow the site. This list changes regularly, but you should be able to find links to my Patreon and Newsletter in it.
Alternatively, I’d appreciate it if you took some time to read some of these related articles:
- Using Python to Visualize the Potential Effects of COVID-19 on Course Evaluations
- How to Approach Python From a Java Perspective
Finally, you may find value in some of these Python resources over on Amazon (ad):
- Effective Python: 90 Specific Ways to Write Better Python
- Python Tricks: A Buffet of Awesome Python Features
- Python Programming: An Introduction to Computer Science
As always, thanks for stopping by and don’t be afraid to say hi! Take care.
Continuing in our computer science problem series, I figured we'd dive into a couple of fun Java topics: parameter passing and primitive types. Specifically, we'll be looking at whether or not you...
Today, I'm kicking off a series of new articles that take a look at real computer science multiple choice questions and attempt to give some tips for solving them. Right now, I want to start by...