avatarMohammed Ayar

Summary

This text discusses four unusual Python code smells and suggests ways to refactor them for better software design.

Abstract

The article emphasizes the importance of software design in programming, highlighting the need to address code smells for maintainable, reusable, readable, and efficient code. It focuses on four unique Python code smells: misuse of lambda functions, long scope chaining, useless exception handling, and the famous range(len(sequence)). For each code smell, the author provides examples and offers suggestions for refactoring the code. The main takeaway is that regular refactoring practices are essential for eliminating bad code smells and ensuring good software design.

Opinions

  1. Misuse of lambda functions can lead to complex and unreadable code, making it less effective.
  2. Long scope chaining, although useful for emulating privacy, becomes problematic when closures are too deep.
  3. Useless exception handling can mask other problems and make it difficult to spot the source of errors.
  4. The range(len(sequence)) code smell can make code vulnerable to bugs due to confusion about the inclusive and exclusive nature of the range() function's arguments.
  5. Regular refactoring practices are necessary to address bad code smells and improve software design.
  6. The author emphasizes the importance of software design in programming, highlighting its role in maintaining and improving existing code.
  7. The article suggests that software design is a subjective and open-ended topic, based on real-life experiences and opinionated perspectives.

Four Unusual Python Code Smells

And how to deal with them.

Photo by Michael Burrows from Pexels modified by Author using Python

When I think about programming and education, I cringe. I cringe because courses address all programming aspects, except one — software design. And sadly, software design is a key element of software development.

“Writing code is not production, it’s not always craftsmanship though it can be, it’s design.” — Joel Spolsky

Without software design, people would be stuck in the 1990s—when software used to be implemented from scratch. Today, however, the smallest startups produce thousands of lines of code, let alone tech giants, game studios, automobile manufacturers and more.

These companies usually base their new software on previous versions of code, which gives room for innovation and creativity. Of course, this would not have been possible without a maintainable, reusable, readable and efficient codebase. These features collectively spell out the essence of good code refactoring practices or anti code smells.

The term “code smell” was coined by fowler and kent in their book, “Refactoring: Improving the Design of Existing Code”. But in reality, the term is nothing more than a cheeky synonym of bad code design.

That said, in this post, we will be addressing four unusual bad code smells and ways to refactor them. They are “unusual” because, to the best of my knowledge, the code smells we are about to discuss are not something you usually stumble upon on the internet. So, I hope you will benefit from them as much as I did. Let’s dive in:

Misuse of Lambda Function

Lambda functions are syntactic sugar for traditional functions. They are anonymous constructs summoned at runtime. Their utility arises in their brevity.

However, when lambda becomes too long to read or too complex to follow, it loses its charm end effectiveness. More importantly, lambda rises and shines in packaging non-reusable code. Otherwise, you are better off with standard functions.

To address different scenarios of lambda's inappropriate usage, we compiled three use cases where lambda starts to smell bad:

#1. Bindings Lambda to Variables

Consider this example:

Good smell:

def f(x, y): 
    return x + y

Bad smell:

f = lambda x, y: x + y

At first glance, one could confidently say that binding lambda to a variable is just as fine as an explicit def declaration. But in reality, this practice is a software anti-pattern because:

  • First, it contradicts the definition of lambda functions. That is, you are giving an anonymous function a name.
  • Second, it smacks the purpose of lambda functions to the wall. That is embedding lambda into larger expressions (PEP 8).

#2. Long Lambda

As of now, it should be obvious that long lambda functions are cues of bad code design. The issue, though, is the heuristic allowing the measurement of such length.

Well, research shows that lambda expressions should obey the following criterion:

  • NOC ≤ 80

NOC: Number of Charachters

Therefore, lambda expressions should not cross 80 characters.

#3. Dirty Lambda

Lambda functions lure many developers, particularly juniors. Its convenience and aesthetic design are likely to drive one to the dirty lambda trap.

You see, lambda is designed to execute one expression. This expression is recommended to have less than a certain number of characters. To dodge this constraint, a couple of dirty hacks and workarounds present themselves.

Nested lambda functions and inner standard functions are notorious dirty workarounds. Let’s take an example to see this up close:

Bad smell:

# inner lambda function
func = lambda x= 1, y= 2:lambda z: x + y + z
f = func()
print(f(4))

Good smell:

def func(x=1, y=2):
    def f(z):
        return x + y + z
    return f
f = func()
print(f(4))

As you can see, although the lambda example was more concise (3 lines VS 6 lines), the lambda code is more confusing and hard to decipher. An example of the confusion caused by this practice is this thread.

Long Scope Chaining

Long Scope chaining is a collection of inner functions that are nested within an enclosing function. The inner functions are technically referred to as closures. The following example provides a clearer picture:

def foo(x):
    def bar(y):
        def baz(z):
            return x + y + z
        return baz(3)
    return bar(10)
print(foo(2))  # print(10+3+2)

Stuffing a function with inner functions is a very attractive solution because it emulates privacy, which is particularly useful and convenient with a language like Python. This is because, unlike C++ and Java, Python is pretty much devoid of private and public class variable distinctions (although there are some hacks). However, this practice starts to smell the deeper the closures are.

To address this, a threshold heuristic for closures has been set out. The metric dictates having a maximum of 3 closures. Otherwise, the code starts to look fuzzy and becomes hard to maintain.

Useless Exception Handling

Exception handlers are a common tool used by programmers to catch exceptions. They are very useful in test code. Yet, they become useless if the exceptions are (1) inaccurate or (2) empty.

#1. Inaccurate Exceptions

The try … except statement gives programmers freedom with regards to managing exceptions. This leads to very general and imprecise exceptions. Take a look at this:

try:
   pass
except Exception:
   raise
# OR
try:
   pass
except StandardError:
   raise

In this example, the exceptions are too general and are likely to signal a broad spectrum of errors making it hard to spot the source of the problem. That is why it is recommended to be precise and specific about exceptions. A good practice is the following example, which specifically aims to signal import errors:

try:
    import platform_specific_module
except ImportError:
    platform_specific_module = None

#2. Empty Exceptions

There is nothing worse than bare exceptions when it comes to error handlers.

Empty except: catches systemExit and KeyboardInterrupt exceptions, rendering program interruption with Ctrl+C harder. Not to mention camouflaging other problems.

To tackle this, Python style guide PEP 8 suggests constraining bare exceptions to 2 use cases:

  • The user wants to flag an error or log the traceback regardless of the nature of the problem.
  • If the user wants to raise exceptions from the bottom to the top,try … finally is a fine alternative.

#3. Remedy?

Unlike the other cited code smells, there is no size-fits-all solution to refactoring exceptions. The generic remedy, however, is to write exception handlers as specifically and carefully as possible.

The Famous Range(len(sequence))

To be frank, range(len()) is a bad habit. It used to be my default looping mechanism. But, I am glad now that I do not remember the last time I have used it.

range(len()) attracts new Python developers. It even magnets experienced developers whose numerical for looping (looping of C++ and the like) is hardwired in their brain. For these people, range(len()) feels like home because it replicates the same looping mechanism as traditional numerical looping.

On the other side of the equation, range(len()) is despised by Python warriors and seasoned developers and therefore considered as an iteration anti-pattern. The reason is that range(len()) makes the code vulnerable to bugs. These bugs largely originate from the fact that the programmers forget that the first argument of range() is inclusive while the second is exclusive.

To address this issue once and for all, we will enumerate common excuses for using range(len()) accompanied by their correct alternative expressions.

  • You require the indices of a sequence:
for index, value in enumerate(sequence): 
        print index, value
  • You want to iterate simultaneously over two sequences:
for letter, digit in zip(letters, digits):
        print letter, digit
  • You want to iterate over a chunk of a sequence:
for letter in letters[4:]: #slicing
        print letter

As you can see, it is possible to avoid range(len()). Still, when the usage of indices of a sequence is beyond the sequence itself (e.g. a function), using range(len())seems a sensible option. For instance:

for x in range(len(letters)):
    print f(x)

Takeaways

Technological advancements brought about significant changes to the way people write and analyse code. For the better. Yet, anti-patterns, code smells, bad code design, you name it, remain a heavily subjective and open-ended topic.

Software design principles are subjective because they are based on real-life experiences and opinionated perspective. That is perhaps the reason why software developers get-togethers cannot pass without software design debates.

For example, some developers find that long lambda functions make codes stink while other developers are in love with lambda and believe that it is pythonic and harmless. I, personally, stand by the side of the former.

In a nutshell, no software can survive without refactoring practices in place, ready to address bad code smells, as goes the saying:

“If it stinks, change it.” — Kent Beck & Martin Fowler

I hope this post could help make your code smell good.

References

Python
Data Science
Machine Learning
Programming
Startup
Recommended from ReadMedium