Python is Beautiful
Michael Renken
Python as a programming language is simply beautiful. This is, without a doubt, the reason I was drawn to it. When I taught myself the language, I was in college and working on my personal projects in Java because that’s what I knew, but I was intrigued by Python because it was much more aesthetically pleasing and was far less rigid. Toward that end, I started doing my Algorithms work in both Java and Python side-by-side in an effort to learn the language.
As an example, let’s compare a basic example: the Fibonacci algorithm. The first example is a Java implementation:
public class Fibonacci {
public static long fibonacci(int n) {
if (n <= 1) {
return n;
}
return fibonacci(n - 1) + fibonacci(n - 2);
}
}
Java works as a language for people who really prefer structure. In Java, it’s important that the procedure for doing things is rigidly enforced and that all types are clearly defined. Part of that is because Java is a compiled language, even if it’s compiled to run on Oracle’s own proprietary virtual environment.
As a contrast, Python is simple. Here, we just have a single function:
def fibonacci(n: int) -> int:
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
Unlike Java, Python is a scripting language, so it’s not necessarily a fair comparision. As such, let’s go ahead and compare it to other scripting languages that existed around the time Python was created. Here’s PHP:
function fibonacci($n) {
if ($n <= 1) {
return $n;
}
return fibonacci($n - 1) + fibonacci($n - 2);
}
And Perl:
sub fibonacci {
my $n = shift;
if ($n <= 1) {
return $n;
}
return fibonacci($n - 1) + fibonacci($n - 2);
}
What Python did was took the simplicity of variable referencing of the C family of languages (which I consider Java a part of), turned it into a scripting language, but then reinvented how blocks of code would be represented. Rather than using symbols like “{”, “$”, etc, Python chose to use white space. In this way, you aren’t forced to break up your implementation with unnecessary symbols that honestly just makes the whole think look cluttered and messy.
Python was also known for what people call “syntactic sugar”, which is a fancy way of saying that they solve problems with syntax rather than adding rigid functions to existing data types. For instance, slices:
>>> a = [1, 2, 3, 4]
>>> a[1:3]
[2, 3]
Negative indexing:
>>> a[-2:]
[3, 4]
List comprehension:
>>> [i for i in range(5) if i >= 2 and i < 4]
[2, 3]
But one of my favorite thing about Python is generators. For most languages,
functions take input parameters and they return a resulting value. Python
allows for the yield keyword to be used to, rather than return a value to the
calling context and scrapping the function context, yield a value to the
calling context and maintain the function context for future yielding:
def animals_i_like():
yield "giraffe"
yield "penguin"
yield "monkey"
Used as a raw generator:
>>> a = animals_i_like()
>>> a.__next__()
'giraffe'
>>> a.__next__()
'penguin'
>>> a.__next__()
'monkey'
For each time the __next__() method is called, we return to the already
in-process function context and continue execution. But this is usually not how
generators are used. Instead, they’re usually iterated over using keywords like
in:
>>> for animal in animals_i_like():
... print(animal)
...
giraffe
penguin
monkey
Or fed into other functions that know how to deal with iterators like:
>>> list(animals_i_like())
['giraffe', 'penguin', 'monkey']
>>> tuple(animals_i_like())
('giraffe', 'penguin', 'monkey')
>>> set(animals_i_like())
{'penguin', 'monkey', 'giraffe'}
But what I find most effective about generator functions is being able to separate out the concerns of determining what things to use and how to use them. So, rather than defining all of that logic in the same loop, you can have the selection logic in your generator and the “do this thing” logic in your loop body:
def find_things() -> Iterator[Thing]:
for thing in get_all_things():
if not can_do_thing(thing):
continue
yield thing
def can_do_thing(thing: Thing) -> bool:
# Can-do logic
def do_things(things: Iterator[Thing]) -> None:
for thing in find_things():
do_thing(thing)
def do_thing(thing: Thing) -> None:
# Do logic
In this way, you can separately implement and test the different parts of your
algorithm. And what’s more, in the above example, the contexts of the
do_things and find_things are maintained in parallel.
This is a very cursory look into my favorite aspects of my favorite programming language, but I think it’s important to be able to verbalize the things you believe, so here we are.