Hole-in-One - Python tricks for oneliner code

Hole-in-One - Python tricks for oneliner code

2024, Jan 05    

Flat is Better Than Nested

I started learning programming, seriously, at the university. With almost no programming knowledge, Python was an excellent introductory language to learn how to tell a computer to do something, writing more complex code, variables, functions, and looping. In my second semester, C++ was the primary language, and the courses focused on a deep understanding of how computers and programs work. You know what I mean: data structures, algorithms, and that scary topic of pointers.

I enjoy learning that stuff, especially implementing data structures on my own. Nonetheless, I started to miss the simplicity of programming in Python, so in my free time, I began to learn more about Python with a friend by solving some of the programming problems we met while learning C++. Quickly, we noticed how simple it was and the tiny size of our code. The more we learn, the more our code shrinks little by little. At one point, Python’s flexibility lets you bend the rules, and we solved most of the problems in just one flat line.

This is a feature-rich programming language, and I continue discovering new features. Python’s dynamic nature allows for some genuinely unique code. In this blog post, I want to share with you some of these features by showing examples of how to use them to create these weird, flat one-liner codes, or as we used to call them, a hole-in-one.

In this blog, you will learn a little bit of:

  • Lambdas functions
  • if-else in one line
  • Short circuits
  • String multiplication
  • Comprehension list
  • The unpacking operator

The Basics

Let me define a hole-in-one (even if it is self-explanatory). It is code that solves a problem and is compacted into a single statement, using multiple statements separated by ; is against the rules, but defining the parameters in a separated line is entirely valid.

Here are basic examples for understanding the idea and exploring some features. The first problem is swapping two variables:

a = 'first'
b = 'second'

# Swap variables: Classic
c = a
a = b
b = c

This is how you can solve the problem in one line:

a = "first"
b = "second"

# Swap variables: Hole-in-one
b, a = a, b

So, the swapping happens in one line, and this is thanks to the flexible tuple creation and unpacking that Python has. Notice that the definitions of the variables are in separate lines, but this is allowed for our definition of the hole-in-one, so there is no problem with that.

Let’s explore other common problems examples:

  • Check if a string is a palindrome:
s = "abcdcba"

# Classic
i = 0
j = len(s) - 1
is_palindrome = True
while i < j:
    if s[i] != s[j]:
        is_palindrome = False
        break
    i += 1
    j -= 1

# Hole-in-one
is_palindrome = s == s[::-1]
  • Check if a list has a repeated value:
l = [1, 5, 4, 8, 1]

# Classic
s = set()
repeated = False
for i in l:
    if i in s:
        repeated = True
        break
    s.add(i)

# Hole-in-one
repeated = len(l) == len(set(l))
  • If the number is even, compute the sum of squares of a list of values:
nums = [0, 3, 1, 4, 5, 2]

# Classic
results = 0
for i in nums:
    if i % 2 == 0:
	    results += i**2

# Hole-in-one
results = sum(map(lambda i: (i % 2 == 0) * i**2), nums)

That should be clear. You may already know the features I used here: string indexing, sets, sum, maps , and lambda. Maybe using a bool*int is new, but that is not all. Python has one or more tricks, which I’ll show you in the following sections.

A Recipe for Recursion

recursion

Recursion is one of my favorite techniques; the ability to reduce complex solutions into a straightforward definition has magic in itself, and of course, with Python lambdas, we can reduce the code further into a single line.

So, the basic recipe for a recursion is this one I use for a Fibonacci series:

def fib(x):
   if x <= 1:  # base case
      return x
   return fib(x-1) + fib(x-2)

Let’s reduce the number of lines step by step. The first feature is the if-else in one line:

def fib(x):
    return x if x <= 1 else fib(x-1) + fib(x-2)

The next natural step is using lambda functions, but there is a little trick to make it work. The problem is that for a recursion, you need a name for the functions to call it inside of its definitions.

n = 10

# We added a function as a parameter
f = lambda x, fib: x if x <= 1 else fib(x-1, fib) + fib(x-2, fib)

# Then pass the same functions as the parameter
(lambda x: f(x, f))(n)

Finally, to achieve the hole-in-one, we need to add f as a parameter of the second function and define the default value. This is its final form:

n = 10

(lambda x, f = lambda x, fib: x if x <= 1 else fib(x-1, fib) + fib(x-2, fib): f(x, f))(n)

This is the recipe for hole-in-one recursion. You can change the default value of f for any other recursive functions so that you can define any other recursion solution in one line, for instance, compute the factorial of a tree search problem.

# Factorial of n
n = 5
(lambda x, f = lambda x, fac: 1 if x <= 1 else x * fac(x-1, fac): f(x, f))(n)

Let’s take it a little bit further and introduce the short-circuits with this example:

# Same tree: Check if p and q are the same tree
# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
p = TreeNode(1, TreeNode(2), TreeNode(3))
q = TreeNode(1, TreeNode(2), TreeNode(3))
(lambda p, q, f = lambda p, q, same: p is q or (p and q and p.val == q.val and same(p.left, q.left, same) and same(p.right, q.right, same)): f(p, q, f))(p, q)

Short circuits allow us to create more compact code by removing the if-else and avoiding running unnecessary checks. It’s a common and efficient way to handle conditional statements in Python.

Some experienced developers like you may think, “Cool! But you can do this in other languages too”, and you are right. The lambda functions and short circuits are features that you can find in almost any other modern language programming.

In the following examples, we will explore the use of Python-only features.

String Manipulation

string

This is the type of problem where you can extract the most value from Python. If you check the String documentation, you’ll find a lot of methods, formatting, and operations defined for strings. Some of these are useful for our lazy mission of just writing one line of code.

Python allows the use of + and * operators with string. The former concatenates two strings, and the latter is used for copying a string n times. This is an example of the classic Christmas tree.

h = 10

# Draw a Chrismas tree with concat and multiply operators
print("".join([" " * (h - i) + "*" * (2*(i-1) + 1) + "\n" for i in range(1, h + 1)]))

# Draw a Chrismas tree with padding
print("\n".join([ ("*" * (2*i+1)).center((h*2)-1) for i in range(0, h)]))

Here is another classic example: the FizzBuzz problem. And I have a solution that combines lots of the previous tricks:

n = 10

# FizzBuzz:
print(*('Fizz'*(not i%3) + 'Buzz'*(not i%5) or str(i) for i in range(1, n)), sep="\n")

This is an excellent example; in this case, you can see the string multiplication and concatenation features when multiplying a boolean, which can be translated into multiplying by 1 or 0. This is followed by a clever short circuit that will return the string version of the number only if the left side of the or returned an empty string—all this inside a list comprehension (actually a generator). Finally, the feature we haven’t seen yet, the unpacking *, which unpacks the list item by item, passes them to the print function, printing each item in a new line thanks to the sep="\n" argument.

To finish this post, I want to share the weirdest example I have done: parse files. In these situations, the lambda default arguments trick becomes handy. But, what I like about these solutions is the use of zip(list) and zip(*list) , for easily reshaping lists.

# A CSV file
f = "example.csv"

# Parse a CSV file into a list of dicts
(lambda csv=[l.strip().split(',') for l in open(f).readlines()]: [dict(zip(csv[0], r)) for r in csv[1:]])()

# Parse a CSV file into a dict of lists
(lambda csv=[l.strip().split(',') for l in open(f).readlines()]: dict(zip(csv[0], map(list, zip(*csv[1:])))))()

Those are all the examples that I have to share for today. There is much more potential in Python, especially with its standard libraries like collections and itertools. Maybe I can show you more in another blog post?

Striking the Balance: Conciseness and Readability

wrapup

Python’s dynamic nature allows for some truly unique and succinct code. Its extensive library support and straightforward syntax enable beginners and experts to perform complex tasks with just a few lines of code. Python is not just a tool but a craftsman’s workshop.

Python’s syntactical sugar isn’t just about making things sweeter; it’s about making them more potent. But remember, while a one-liner can be satisfying, it’s not always the best approach.

We can do bizarre things, but remember that writing good code doesn’t mean writing less; it means writing code that is easy to read, efficient, and maintainable. The beauty of Python lies in its readability. It’s often called ‘executable pseudocode’ because it is clear and expressive. While I enjoyed writing the hole-in-one solutions, I don’t do it or recommend doing it in production code.

Most of the time, writing more is better. Consider this scenario - you’re working with a list of data, and you need to filter and process it. A one-liner list comprehension might seem like a neat solution, but it can quickly become unreadable with multiple conditions and transformations. In such cases, breaking down the process into a multi-line for-loop with clear variable names and comments can save you and your team a lot of headaches. It’s about finding the right balance between brevity and clarity.