Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

This lesson is for members only. Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

Build the Maze

00:00 Building the maze. At the very core, a maze is an ordered collection of squares, which you can represent with a Python tuple. However, you’ll eventually want to augment your maze model with additional properties and methods, so it makes sense to wrap the sequence of squares in a custom class right away.

00:21 Enter the code seen on-screen into the maze module.

00:34 Here, you use an immutable data class again to ensure that the underlying tuple of Square objects remains unchanged once assigned. You might be inclined to use a Python list instead of a tuple to keep your squares, but that would prevent you from caching partial results of your computations later. Python’s cache requires memoized function arguments to be hashable and therefore immutable. To avoid the extra work when looping over the squares or when accessing one of them by index, you can make your class iterable and subscriptable by implementing these two special methods.

01:15 This method lets Maze instances cooperate with a for loop, while the second one enables square bracket notation for getting squares by index.

01:35 Next, you might want to calculate the width and height of the maze, knowing the column and row indices of the underlying squares.

01:54 You take advantage of the iterable nature of the maze by iterating over it to find the maximum column and row index of its squares with the help of the max() function.

02:03 Adding 1 to the highest index accounts for the zero-based numbering of tuple indices. Because looping is a relatively expensive operation, you cache the return values with functools.cached_property instead of using the built-in @property decorator. As a result, the width and height are computed only once on demand, while their subsequent invocations will return the cached value.

02:31 The benefit of calculating the maze size by hand is data consistency. If you supplied the width and height through two extra parameters in the class, then there would be no guarantee that the rows and columns would match up with the flat index.

02:45 So inferring the size from the squares avoids this potential problem. Speaking of consistency, you can also include the validation of the maze by looping over it again when it’s created to make sure that its squares have the the expected rows and columns with matching indices. To do that, you’ll leverage the special method .__post_init__() to hook into the initialization process of the data class.

03:28 The first function checks whether the .index property of each square fits into a continuous sequence of numbers that enumerates all the squares in the maze.

03:47 The second function iterates over the rows and columns in the maze, and this ensures that the .row and .column attributes of the corresponding square match up with the current row and column of the loops. Watch out for proper indentation of these functions, as they don’t belong in the class body.

04:05 Both validation functions rely on the assert statement to raise the AssertionError and prevent the maze from being created in the case of invalid data.

04:17 Earlier, you decided that a maze must have an entrance and an exit, so it’s worth confirming that it has both. Go ahead and add two more validation functions.

04:32 First Role is imported, as this will be needed in the new validation functions. Then the validation functions are added to the .__post_init__() method.

04:47 Finally, the functions are added to the bottom of the file. These count the number of squares whose role is either ENTRANCE or EXIT and verify that there’s exactly one of each.

05:28 For your convenience, you might as well implement the relevant properties that will return squares with those special roles.

05:47 This code calls next() on a generator expression that filters the squares by their role. Because you already validated them, you can safely assume that the appropriate squares exist in the maze, and next() won’t raise any exception.

06:07 At long last, you can finally build your first maze using the building blocks defined earlier. This maze will be needed a number of times in this course, so to save having to type it out multiple times in a REPL session, you’ll put it inside the mazes/ directory at the top level of the project, at the same level as the src/ folder.

06:28 This will keep you out to the way of the main project, but it will mean you’ll need to start your REPL session at that level later on when you want to import it.

06:58 This sample maze has twelve squares with ten unique border patterns, arranged in three rows and four columns. The entrance to the maze is located in the bottom-left corner, while the exit is in the first row, slightly to the right.

07:12 These squares are created at index two and eight.

08:16 Being able to visualize the final result just by looking at the code is quite a niche skill. So, on-screen you can see what the maze would look like. It may not be a masterpiece, but having a small dataset sample to work with is beneficial for several reasons. When something doesn’t work as expected, you’ll be able to spot the problem much quicker.

08:36 You’ll also have a better understanding of the underlying concepts, allowing you to debug code more efficiently. Finally, because there’s little data to process, you’ll spend a lot less time waiting for the results in your development cycle.

08:52 So far, you’ve identified the building blocks of the maze and implemented them in Python, but now you can build a maze, the next step will be to figure out how to display it in a graphical form, and that’s what you’ll be doing in the next section of the course.

stefaan1o on July 23, 2023

What is the reason for putting the validation functions outside the class? Does this have an advantage?

Bartosz Zaczyński RP Team on Aug. 7, 2023

@stefaan1o The main advantage is the separation of concerns, allowing you to change the validation logic independently from the class itself. Having them in two separate places lets you reuse the validation functions elsewhere without having to instantiate the class, which might become irrelevant in a new context. It also makes the code more modular and easier to read, as well as to unit test the individual functions.

Become a Member to join the conversation.