How I refactored maze generating Python code and used advanced class features

How I refactored maze generating Python code and used advanced class features

Previously, I have created a maze generating code while learning Python. Later I enhanced it with animation and marking special cells. All of it was done in rush, to solve the problem. Now I spent some time improving the quality of the code by encapsulating code into classes, using “private” methods and fields and separating concerns. The exact steps I took can be viewed in the GitHub repository or read here.

I’ll list all commits of the repository, put their descriptions and link for full preview in GitHub.

(Quick tip – git log)

I have generated this page using some tricks of git log and WordPress’ Gutenberg editor.

It is possible to use some Markdown syntax when pasting content into the editor. The shortcuts I used were:

  • ## header creates a header
  • `code` creates a code
  • [link](https://lukasznojek.com) creates a link

Although the first two work also as you type text in the editor, the last one for some reason doesn’t – only when pasting the text (and only as plain text).

It is possible to customize the format of the output of git log command – check the placeholders section in git log documentation.

I used a relatively simple format:

git log --pretty=format:"## %s%n[See the diff](https://github.com/lukaszmn/maze-recursive-backtracker/commit/%H)%n%n%b" --reverse >log.txt

What do these options mean?

  • ## %s returns ## and subject of the commit. ## will convert that line to a header in WordPress
  • %n adds a new line to separate entries
  • [See the diff](https://.../%H) creates a link named See the diff with URL that ends with full hash of the commit
  • %n%n adds another two new lines. A single new line would not create a new paragraph, but would continue the same paragraph from a new line
  • %b adds the body of the comit
  • --reverse starts with the oldest commits and heads onto the newest
  • >log.txt saves the output to the log.txt file

This command generated the following output (fragment):

## Extracted FarthestDeadEnd
[See the diff](https://github.com/lukaszmn/maze-recursive-backtracker/commit/fecc4d453a69c41dfca948c17ab673340618162c)

The position of the farthest dead end and the entire drawDeadEnd()
method were extracted to new class FarthestDeadEnd.

## FarthestDeadEnd - included getIncreasingRadius
[See the diff](https://github.com/lukaszmn/maze-recursive-backtracker/commit/e32c467f1aedab0689e744a539a185aa37bede78)

The getIncreasingRadius() method was included into the FarthestDeadEnd
class, because it was used only by it.

I only had to manually unwrap lines and basically it was done.

Now, on to describing the refactoring!

Refactoring

First version

See the code

This commit was described in the first post of the series.

Added visualization

See the diff

This commit was described mostly in the second post of the series.

Extracted StepController

See the diff

Lines that were responsible for counting steps and checking step limit were extracted to a StepController class.

Extracted FarthestDeadEnd

See the diff

The position of the farthest dead end and the entire drawDeadEnd() method were extracted to new class FarthestDeadEnd.

FarthestDeadEnd – included getIncreasingRadius

See the diff

The getIncreasingRadius() method was included into the FarthestDeadEnd class, because it was used only by it.

Extracted CurrentCell

See the diff

Now it’s a little more complicated, as there were no methods that could be just copied. The newly created CurrentCell class should store location of the cell that’s currently analyzed. It also stores the marker object used for drawing yellow circle.

CurrentCell – added moveForward

See the diff

The else clause was extracted as moveForward() method of CurrentCell.

Extracted MazeRules

See the diff

The methods getNextCell(), getValidDirections() and validCell() consistuted for a new class MazeRules.

MazeRules – added Result

See the diff

Instead of returning some magic numbers from getNextCell() method, an object Result is returned with all information being clear. The code is longer now, but that’s a growth worth the clarity.

CurrentCell – added goBack

See the diff

It’s time now to extract the first if clause into the goBack() method of CurrentCell class.

CurrentCell – added getDecreasingRadius

See the diff

Since getDecreasingRadius() method was used only by CurrentCell, it made sense to include it in that class.

Extracted MazeDrawer

See the diff

Most code related to drawing was extracted to MazeDrawer class. By the way some routines were named to explain what they are doing, e.g. drawBackgroundAndBorder(). In order to limit graphic operations to one class, it was passed to the constructor of CurrentCell.

Refactoring – size_x, size_y as private fields

See the diff

Some attempt was made to reduce the use of global size_x and size_y variables. And, by the way, private fields size_x and size_y were prefixed with double underscore __.

Refactoring – Marking private members (1)

See the diff

Continuing prefixing private members of classes with double underscore __.

Refactoring – Marking private members (2)

See the diff

Continuing prefixing private members of classes with double underscore __. It also enforced adding some getters that would access the now-private members, like get_x().

MazeRules – added getPreviousCell

See the diff

The logic of backtracking was moved into getPreviousCell() method of MazeRules class.

MazeRules – added visitedMaze, visitedCell and visitCell

See the diff

There were still some loose global variables and methods. Some could be very neatly included into MazeRules class. This also made a dependency onto that class for other: CurrentCell and FarthestDeadEnd now require a reference to MazeRules to get the path’s length.

generateMaze – included mazeDrawer

See the diff

This is the final step to get rid of global size_x and size_y variables. The MazeDrawer object is now created inside generateMaze, not outside of it.

Extracted MazeDrawer to a file

See the diff

This step could have been done earlier, when creating the MazeDrawer class. It decreases the amount of lines of code in the main file, makes finding classes easier, and decreases imports in each file to only the ones that are necessary.

Extracted MazeRules to a file

See the diff

Extracted FarthestDeadEnd to a file

See the diff

Extracted StepController to a file

See the diff

Extracted CurrentCell to a file

See the diff

Fixed saving animation frames

See the diff

As it turned out, after all refactorings the frame saving routine was not working. First of all, it was in incorrect class – now is in MazeDrawer as saveFrame() method. It required also access to current step, which was exposed in get_step() method of StepController class.

Increased image size

See the diff

The previous scale of drawing (plot) was good to have code and plot side-by-side in Spyder IDE. The size stopped being enough when I created an animation. It was small and the quality was poor. Here I doubled the size and increased the font’s size.

Fixed FarthestDeadEnd.__getIncreasingRadius: removed variable duplication

See the diff

One thing was omitted during refactoring – the distance was passed as argument to the __getIncreasingRadius() method, and it was also calculated, overwriting that argument. The unnecessary calculation was removed here.

And that’s it as far as the refactoring goes. In the further paragraphs I will list new things I learned during the process of refactoring.

Classes in Python

An example of class in Python:

Python

My observations:

  1. The contructor is named __init__. Documentation.
  2. Every method inside the class must have the first parameter that will refer to the class. Usually it’s named self, but any name is acceptable. Documentation.
  3. You cannot list the class’ fields (variables) to have control over what belongs to it. You define them in the place you want to use them – so it’s easy to create the fields in any methods, which will make the code more difficult to maintain. It’s a good idea to declare and initialize all fields in the constructor.
  4. “Private” fields and methods are by convention prefixed with double underscore __. This is just an information that member should not be used by other classes, but there is no mechanism that would prevent from calling e.g. stepController._StepController__max_steps = 4 (note the way to access the field from outsisde). Documentation.
  5. There are no setters and getters special properties in Python, but there is a convention to name them set_foo() and get_foo().

Inner classes and static methods in Python

Python

My observations:

  1. Create the inner class as a class member with keyword class.
  2. Instantiate the class from the outer class (MazeRules) by referring to it with self.Result. Nothing unexpected here.
  3. Create a static method with attribute @staticmethod. There are no magic parameters. I created the parent parameter, which as you can see in line 4 or 6, passes self, which allows me to create the Result object by calling parent.Result (i.a. self.Result from the context of getNextCell() method). It’s a bit complicated, but it works well.

Lessons learned

Constructor in Python is __init__

Every class’ method must have self first parameter

Python class defines fields inside methods, like JavaScript

There is convention to name getters as get_foo()

Python won’t warn about unused or overwritten variables

It’s possible but complicated to create inner class and static methods

Git log can be used to generate pages

Leave a Reply

avatar
  Subscribe  
Notify of