Python - A Simple Method for Refactoring Your Code For Extensibility

Published on April 15, 2018, 7:40 a.m.

Updated on April 10, 2020, 3:23 a.m.!

This article is for the beginner. Or if you're only used to procedural programming. Or to someone who uses Python just to write simple scripts. What I want to do is show you a way to refactor your scripts once you're able to get them to work. This won't necessarily speed up your script but will optimize your code for better organization and readability. When you organize your code and make it more readable, it may actually make it easier to find ways to speed up its execution.

The following scripts will show you this progression. Be aware that it will be very simple. This script will ask the user his/her name and will render a greeting back to them using their name. Each script will have the same functionality but will just be written in a progressively different way.

Should I Avoid premature optimization or not?

Ok, to get this part out of they way ... if you've read much on the subject of programming you have likely come across this saying:

"Premature optimization is the root of all evil (or at least most of it) in programming."

This saying is attributed to Donald Knuth who is a famous computer scientist. Certainly this has some truth to it (especially for compiled languages) but I think it may need some further clarification.

If I'm creating a full-fledged application from the ground up, I am going to optimize the orgranization and readability from beginning as much as I possibly can but I'm not going to worry about it being too perfect.

If I'm writing a script to do one main thing well, then I'm just going to start off with a procedural script and then refactor from there. This example will show the process as we go.

Write one module to do something really well

Here is the first demo script (demo1.py):

1
2
3
4
5
6
7
#!/usr/bin/env python

# Ask user's name.
name = input('What is your name? ')

# Render a greeting to the user.
print('Hello {}, it is nice to meet you!'.format(name))

If you run it from the command line, it will look like this:

# ./demo1.py 
What is your name? Bob
Hello Bob, it is nice to meet you!

This is written in a procedural style. It's not really even functional programming or object-oriented as of yet. It's very simple to follow.

The script only does:

  1. Ask the user's name
  2. Greet the user with "Hello Bob, it is nice to meet you."

This is fine but if you needed to add some functionality to it, you would have to add more statements in between each line. However, after say about 50-100 lines or so -- which could happen pretty quickly for most scripts, it would start to get tedious and more difficult to read.

Once it gets to be about 200 lines or so, it gets to be more difficult to manage. And after around 1000 lines, it can start to become very difficult to maintain.

Refactor module to use "if name == 'main'"

For now, let's just add something new (demo2.py):

1
2
3
4
5
6
7
8
#!/usr/bin/env python

if __name__ == '__main__':
    # Ask user's name.
    name = input('What is your name? ')

    # Render a greeting to the user.
    print('Hello {}, it is nice to meet you!'.format(name))

The functionality doesn't change. It's still going to work as it has already worked but now we're putting the code into our main function. It's just going to check to see if this script was actually called from the file system like: python demo2.py.

Moving on.

Functions are good. They help us segment a routine of code into a nice and easily maintainable block. In fact, let's add one ...

Wrap your script into a function

This time, we'll still use our main function but let's take the main code and put it into its own function called greet() (demo3.py).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#!/usr/bin/env python


def greet():
    # Ask user's name.
    name = input('What is your name? ')

    # Render a greeting to the user.
    print('Hello {}, it is nice to meet you!'.format(name))


if __name__ == '__main__':
    greet()

Now, not only is it still working as before, but if we wanted to, we could call this module from another script like so:

import demo3

demo3.greet()

This actually adds functionality and value to our script. This is called re-usability which is a big advantage of writing object-oriented code.

Use messages instead of string literals when it makes sense to do so

This part is something that may be more optional for you but personally, I prefer to put my string literals into a constant. One can really get bogged down into this but it's always a good idea to keep your code from repeating itself including using the same string literals. This concept is not a big deal really for this demo script but it's a good example for clarity.

Even though Python doesn't support pure constants, It's still a good idea to use all caps for a variable so that others who may have to maintain our code knows the value shouldn't change over the course of the script.

Here's my example (demo4.py):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#!/usr/bin/env python

ASK_NAME_MSG = 'What is your name? '
GREETING = 'Hello {}, it is nice to meet you!'


def greet():
    # Ask user's name.
    name = input(ASK_NAME_MSG)

    # Render a greeting to the user.
    print(GREETING.format(name))


if __name__ == '__main__':
    greet()

All we've done here is move the name asking string to a variable called ASK_NAME_MSG and the greeting string to another variable called GREETING.

Again, this is kind of optional but remember DRY (Don't Repeat Yourself).

Our next refactoring involves breaking our greet() function into two separate parts ...

Break big function into smaller functions and consider using run()

Here, we'll create a new function called get_name() and change our greet() function to only handle the greeting (demo5.py):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/env python

ASK_NAME_MSG = 'What is your name? '
GREETING = 'Hello {}, it is nice to meet you!'


def get_name():
    return input(ASK_NAME_MSG)


def greet(name):
    print(GREETING.format(name))


def run():
    # Ask user's name.
    name = get_name()

    # Render a greeting to the user.
    greet(name)


if __name__ == '__main__':
    run()

As you can see, the get_name() function gets the user's name. Also, the greet() function only greets and doesn't do anything else. I've also created what you'll find to be something very standard in many Python scripts: a function called run().

Why run()? Well, it's not required but if you're using special features in Python like multithreading for example, you will need to call a threaded object like so:

thread = MyThreadedClass()
thread.start()

A threaded class will run with start(). This is because internally the thread's start() method looks for a function called run(). Now, you can call this run() or start() or initiate() if you like, but run() is more of a convention with many Python modules that need to actually run.

Here's where we can take everything we've done so far and turn our greeting script into a Greeter object.

Combine common functions in an object

Once we've created a number of common functions, it eventually makes sense to create an object that can use all of these functions together. A couple of huge benefits of object-oriented programming (OOP) is reusability and extensibility (that is being able to add new functionality as needed).

This refactoring won't be much of a stretch since we've done quite a bit of the work already (demo6.py):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/env python

ASK_NAME_MSG = 'What is your name? '
GREETING = 'Hello {}, it is nice to meet you!'


class Greeter:
    def get_name(self):
        return input(ASK_NAME_MSG)

    def greet(self, name):
        print(GREETING.format(name))

    def run(self):
        # Ask user's name.
        name = self.get_name()

        # Render a greeting to the user.
        self.greet(name)


if __name__ == '__main__':
    app = Greeter()
    app.run()

We're already using the constant strings (or messages) at the top. We haven't changed those. We're also using the exact same functions excepts that now they are members of our new class called Greeter (which is an object). Once they become members of a class, they are no longer called functions but are now called methods.

Keep in mind too that we could have moved the ASK_NAME_MSG and GREETING into the class and make them class fields but that's your choice. Just know that you would probably won't change the case to lower and you would need to access them within the methods as self.ask_name_msg and self.greeting.

In the main function, we instantiate a Greeter object called app and then we use app to call the method "run()".

As I mentioned, I don't always create really big scripts or applications themselves using this same method of refactoring but if you think about it, it does make the approach to writing object-oriented code to more organized. Code organization helps with readability and the ability to build on it.

Hopefully this was helpful.

If this blog is helpful, please consider helping me pay it backward with a coffee.

Buy Me a Coffee at ko-fi.com