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.
Table of contents
- (Quick tip – git log)
- Refactoring
- First version
- Added visualization
- Extracted StepController
- Extracted FarthestDeadEnd
- FarthestDeadEnd – included getIncreasingRadius
- Extracted CurrentCell
- CurrentCell – added moveForward
- Extracted MazeRules
- MazeRules – added Result
- CurrentCell – added goBack
- CurrentCell – added getDecreasingRadius
- Extracted MazeDrawer
- Refactoring – size_x, size_y as private fields
- Refactoring – Marking private members (1)
- Refactoring – Marking private members (2)
- MazeRules – added getPreviousCell
- MazeRules – added visitedMaze, visitedCell and visitCell
- generateMaze – included mazeDrawer
- Extracted MazeDrawer to a file
- Extracted MazeRules to a file
- Extracted FarthestDeadEnd to a file
- Extracted StepController to a file
- Extracted CurrentCell to a file
- Fixed saving animation frames
- Increased image size
- Fixed FarthestDeadEnd.__getIncreasingRadius: removed variable duplication
- First version
- Classes in Python
- Inner classes and static methods in Python
- Lessons learned
(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:
E-book: How to set up free WordPress platform for IT blogs
Do you want to set up a typical blog but you don’t know how to start, which plugins to use or how to customize the site? I spent a few weeks in total doing that from scratch – from learning the ecosystem to adjusting the platform to my very specific needs. Now I want to share with my experience and make the start easier for you.
## 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 thelog.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 entiredrawDeadEnd()
method were extracted to new classFarthestDeadEnd
. ## FarthestDeadEnd - included getIncreasingRadius [See the diff](https://github.com/lukaszmn/maze-recursive-backtracker/commit/e32c467f1aedab0689e744a539a185aa37bede78) ThegetIncreasingRadius()
method was included into theFarthestDeadEnd
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
This commit was described in the first post of the series.
Added visualization
This commit was described mostly in the second post of the series.
Extracted StepController
Lines that were responsible for counting steps and checking step limit were extracted to a StepController
class.
Extracted FarthestDeadEnd
The position of the farthest dead end and the entire drawDeadEnd()
method were extracted to new class FarthestDeadEnd
.
FarthestDeadEnd – included getIncreasingRadius
The getIncreasingRadius()
method was included into the FarthestDeadEnd
class, because it was used only by it.
Extracted CurrentCell
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
The else
clause was extracted as moveForward()
method of CurrentCell
.
Extracted MazeRules
The methods getNextCell()
, getValidDirections()
and validCell()
consistuted for a new class MazeRules
.
MazeRules – added Result
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
It’s time now to extract the first if
clause into the goBack()
method of CurrentCell
class.
CurrentCell – added getDecreasingRadius
Since getDecreasingRadius()
method was used only by CurrentCell
, it made sense to include it in that class.
Extracted MazeDrawer
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
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)
Continuing prefixing private members of classes with double underscore __
.
Refactoring – Marking private members (2)
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
The logic of backtracking was moved into getPreviousCell()
method of MazeRules
class.
MazeRules – added visitedMaze, visitedCell and visitCell
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
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
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
Extracted FarthestDeadEnd to a file
Extracted StepController to a file
Extracted CurrentCell to a file
Fixed saving animation frames
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
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
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:
My observations:
- The contructor is named
__init__
. Documentation. - 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. - 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.
- “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. - There are no setters and getters special properties in Python, but there is a convention to name them
set_foo()
andget_foo()
.
Inner classes and static methods in Python
My observations:
- Create the inner class as a class member with keyword
class
. - Instantiate the class from the outer class (
MazeRules
) by referring to it withself.Result
. Nothing unexpected here. - Create a static method with attribute
@staticmethod
. There are no magic parameters. I created theparent
parameter, which as you can see in line 4 or 6, passesself
, which allows me to create theResult
object by callingparent.Result
(i.a.self.Result
from the context ofgetNextCell()
method). It’s a bit complicated, but it works well.