You Should Stop Craving for Python One-Liners
By: Mohammed Ayar
Tags: One Liners, Programming, Python, Coding Style, Efficiency

Only recently and while working on a machine learning program, one of my class functions had to return a list of employees with the highest salary from a dictionary mapping employees' IDs to their salaries. We’ll use that as an example.

For the sake of demonstration and simplicity, let’s consider the following dictionary mapping employees’ names to their annual salaries:

As a practice, I recommend that you try to find the solution on your own then you can compare it with my solutions below.

Having said that, let’s dive into it.

Readability

On the first attempt, I came up with the following test.py :

>>> high_paid_employees()

>>> ['Anthony', 'Carla']

Although this version makes use of simple and basic Python syntax, most intermediate and seasoned programmers will most likely reject it outright. This is because the program is too long. And length impacts negatively code readability.

Besides, while this program does the job, in the world of programming, it is known that a simple code should be short and to the point, which leads us to one-liners.

By sticking two nested list comprehensions in the return statement we could write a one-liner one_liner.py that would allow us to do exactly what 10 lines of code test.py did.

>>> high_paid_employees()

>>> ['Anthony', 'Carla']

There are two main problems arising from this one-liner. First, it doesn’t adhere to the DRY (don’t repeat yourself) principle — string manipulation was performed 2 times. Second, since a decent amount of lines were squeezed into one expression, the code became cryptic and hard to decipher at first glance, even for people who are familiar with Python.

Tip: I didn’t write the return statement in one single line so as to respect the threshold suggested in the Python code style guidelines. And according to the guidelines, each line should not exceed 79 characters.

In a nutshell, while one_liner.py represents a shorter and concise solution to the task at hand, it fails to meet the requirements of what constitutes simple and readable code. Thus, it’s a badly refactored code.

Maintainability

Maintainability in programming means the ease with which a code can be re-engineered to serve some other purposes. An example of that would be rather than getting the names of employees with the highest paycheck we want to get those with the lowest salary instead.

For that purpose, we can see that a bit of work is needed on test.py . For instance, highest_salary and the first if statement should be reprogrammed again, which might be time-consuming. And in the business world time is money.

On the other side of the spectrum, one_liner.py seems to be more maintainable as it suffices to change the max() function to min() and you end up with employees with the lowest paycheck.

Memory

A good programmer should heed the importance of memory.

Memory is the amount of space that’s being allocated to store data and instructions while a program is running. So, when a program is memory efficient, it takes little space in memory.

We’ll use the memory profiler module in order to analyze the memory usage in both programs. The memory profiler is easy to use as it’s enough to decorate the function you want to monitor with @profile decorator to get the summary.

The memory profiler applied on test.py yields the following:

$ python -m memory_profiler test.py

Filename: test.py

============================================================

10 19.191 MiB 19.191 MiB 1

11 def high_paid_employees():

12 19.195 MiB 0.004 MiB 1 dict_sal = salaries.copy()

13 19.195 MiB 0.000 MiB 1 high_sal = 0

14 19.199 MiB 0.000 MiB 7 for employee in dict_sal:

15 19.199 MiB 0.004 MiB 6 dict_sal[employee] = int(dict_sal[employee].rstrip("k"))

16 19.199 MiB 0.000 MiB 6 if dict_sal[employee] > high_sal:

17 19.199 MiB 0.000 MiB 2 high_sal = dict_sal[employee]

18

19 19.199 MiB 0.000 MiB 1 high_paid_emp = []

20 19.199 MiB 0.000 MiB 7 for employee in dict_sal:

21 19.199 MiB 0.000 MiB 6 if dict_sal[employee] == high_sal:

22 19.199 MiB 0.000 MiB 2 high_paid_emp.append(employee)

23

24 19.199 MiB 0.000 MiB 1 return high_paid_emp Line # Mem usage Increment Occurences Line Contents============================================================10 19.191 MiB 19.191 MiB 1 @profile 11 def high_paid_employees():12 19.195 MiB 0.004 MiB 1 dict_sal = salaries.copy()13 19.195 MiB 0.000 MiB 1 high_sal = 014 19.199 MiB 0.000 MiB 7 for employee in dict_sal:15 19.199 MiB 0.004 MiB 6 dict_sal[employee] = int(dict_sal[employee].rstrip("k"))16 19.199 MiB 0.000 MiB 6 if dict_sal[employee] > high_sal:17 19.199 MiB 0.000 MiB 2 high_sal = dict_sal[employee]1819 19.199 MiB 0.000 MiB 1 high_paid_emp = []20 19.199 MiB 0.000 MiB 7 for employee in dict_sal:21 19.199 MiB 0.000 MiB 6 if dict_sal[employee] == high_sal:22 19.199 MiB 0.000 MiB 2 high_paid_emp.append(employee)2324 19.199 MiB 0.000 MiB 1 return high_paid_emp

On the other hand, the memory profiler on one_liner.py produces the following results:

$ python -m memory_profiler one_liner.py

Filename: one_liner.py

============================================================

9 19.285 MiB 19.285 MiB 1

10 def high_paid_employees():

11 19.297 MiB 0.008 MiB 10 return [

12 19.297 MiB 0.000 MiB 3 emp for emp in salaries

13 19.297 MiB 0.004 MiB 12 if int(salaries[emp].rstrip("k")) == max(

14 19.297 MiB 0.000 MiB 54 [int(sal.rstrip("k")) for sal in salaries.values()]

15 )

16 ] Line # Mem usage Increment Occurences Line Contents============================================================9 19.285 MiB 19.285 MiB 1 @profile 10 def high_paid_employees():11 19.297 MiB 0.008 MiB 10 return [12 19.297 MiB 0.000 MiB 3 emp for emp in salaries13 19.297 MiB 0.004 MiB 12 if int(salaries[emp].rstrip("k")) == max(14 19.297 MiB 0.000 MiB 54 [int(sal.rstrip("k")) for sal in salaries.values()]15 )16 ]

Even though the first code consisted of more code it’s used less memory than its one-liner counterpart (19.199 MB Vs 19.297 MB). This is due to the substantial amount of stored instructions in one_liner.py . Therefore, memory-wise one_liner.py is not a suitable option.

Runtime

Another essential factor to consider is Runtime, which is the time a program takes to finish execution. This becomes problematic when we’re dealing with a huge amount of data — 10 000+ entries. These are likely to be real-life data.

One of the best ways to calculate a program’s running time in Python is the timeit module.

The following command gives the processing time of test.py in microseconds:

$ python -m timeit -s "from test import high_paid_employees" "high_paid_employees()"

50000 loops, best of 5: 2.67 usec per loop

Similarly, one_liner.py yields the results below:

$ python -m timeit -s "from one_liner import high_paid_employees" "high_paid_employees()"

50000 loops, best of 5: 13.4 usec per loop

Again, test.py outperformed one_liner.py . This is because of the ample amount of calculations one_liner.py has gone through.

The sweet spot

To improve upon the last metrics, we can come up with a set of instructions that are neither excessively lengthy nor extremely short. The code is the following sweet_spot.py :

By minimizing the length of this code and leveraging Python's powerful syntax we managed to create a short and readable code. Each line does one important task at a time. Data cleaning, finding the max, and filtering.

As to maintainability, this code version is highly maintainable since it is enough to tweak the code a bit to get different results that might satisfy other purposes.

Finally, the Benchmark of sweet_spot.py using timeit and memory_profiler output the following:

$ python -m memory_profiler sweet_spot.py

Filename: sweet_spot.py Line # Mem usage Increment Occurences Line Contents ============================================================ 11 19.219 MiB 19.219 MiB 1 @profile 12 def high_paid_employees(): 13 19.227 MiB 0.004 MiB 10 clean_salaries = { 14 19.223 MiB 0.004 MiB 1 emp: int(sal.rstrip("k")) for emp, sal in salaries.items() 15 } 16 19.227 MiB 0.000 MiB 1 max_sal = max(clean_salaries.values()) 17 19.227 MiB 0.000 MiB 1 return [ 18 19.227 MiB 0.000 MiB 1 filter(lambda x: clean_salaries[x] == max_sal, clean_salaries) 19 ] $ python -m timeit -s "from sweet_spot import high_paid_employees" "high_paid_employees()" 100000 loops, best of 5: 2.62 usec per loop

We can see that test.py got the upper hand in terms of memory compared to one_liner.py and sweet_spot.py . In term of execution time, test.py and sweet_spot.py perform equally and both are faster than one_liner.py .