Follows: Lecture 29 - Objects II: Methods, Aliasing, and Collections
In the tracing lab, you observed how a class defines the attributes of its instances and how assignments change object state. In Lecture 29, you learned that methods define operations on that state, that variables can refer to the same object, and that collections can hold references to objects.
In this lab, you will write and test code that combines those ideas. You will implement:
- a query method that computes a value without changing the object;
- a mutating method that updates an object's attributes;
- two functions that process lists of objects; and
- methods on an object that contains a list of other objects.
The lab has four programming tasks. For every task, write multiple unit tests and run the full test suite. Debugging is a tool for fixing code, but tracing tables are not required.
- Design, implement, and test functions and methods operating on compound data.
- Distinguish query methods from methods that change object state.
- Write tests that account for object references, identity, and aliasing.
- Process lists of objects without unintentionally copying or modifying them.
- Implement a class whose instances contain collections of other objects.
-
Clone the repository in your IDE and open the project.
-
You will work with these files:
data.py data_tests.py lab.py lab_tests.py -
Review the supplied
Pointclass indata.py. It hasxandyattributes. The instructor's starter version for this lab will also provide the method signatures andPointPathskeleton described below. -
Run the provided tests before making changes.
-
Implement the methods for Tasks 1, 2, and 4 in
data.pyand test them indata_tests.py. Implement the functions for Task 3 inlab.pyand test them inlab_tests.py.
The provided Time class, if present in the repository, is not part of this lab. Do not modify it.
A point in two-dimensional space has two attributes:
p = data.Point(3.0, 4.0)
# p.x is 3.0 and p.y is 4.0For the exercises below, data.py should have these unfinished method signatures (the instructor will provide this starter structure):
class Point:
def __init__(self, x: float, y: float):
self.x = x
self.y = y
def distance_to(self, other: "Point") -> float:
raise NotImplementedError
def translate(self, dx: float, dy: float) -> None:
raise NotImplementedError
class PointPath:
def __init__(self, name: str):
self.name = name
self.points: list[Point] = []
def add_point(self, point: Point) -> None:
raise NotImplementedError
def total_length(self) -> float:
raise NotImplementedErrorDo not replace these methods with ordinary functions. Implement them inside their classes.
For each task:
- State the purpose, inputs, outputs, and whether the operation changes object state.
- Identify relevant attributes and existing functions or methods.
- Work through examples and important boundary cases.
- Write multiple unit tests, including tests for any promised non-mutation or reference behavior.
- Complete the implementation and run all tests.
- Investigate failures, correct the code, and run all tests again.
Implement the distance_to method in Point.
def distance_to(self, other: "Point") -> float:It must return the Euclidean distance from self to other, without changing either point. For points (x1, y1) and (x2, y2), the distance is:
sqrt((x2 - x1)**2 + (y2 - y1)**2)
You may import sqrt from math.
Example:
first = data.Point(0, 0)
second = data.Point(3, 4)
result = first.distance_to(second)
# result is 5.0Write multiple tests in data_tests.py. Include points with negative coordinates, a point compared with itself, reversing argument order, and confirmation that neither point's attributes change.
- What does
selfrefer to duringfirst.distance_to(second)? - What does
otherrefer to? - Why is this a query method rather than a mutating method?
Implement translate in Point.
def translate(self, dx: float, dy: float) -> None:It must change the existing point by increasing self.x by dx and self.y by dy. It must not construct a replacement point. It returns None.
Example:
p = data.Point(2, 5)
p.translate(3, -2)
# p.x is 5 and p.y is 3Write multiple tests in data_tests.py that check positive, negative, and zero offsets. Include both of the following reference situations:
p = data.Point(2, 5)
alias = p
alias.translate(3, -2)
# Both names still refer to the same, now-modified point.first = data.Point(2, 5)
second = data.Point(2, 5)
first.translate(3, -2)
# The separate second point should not change.Use self.assertIs(...) to check shared identity, and check attribute values to confirm which object changed.
- Why does translating
aliasalso change the state observed throughp? - Why does translating
firstnot changesecond? - What is the difference between a query method and a mutating method?
Implement two functions in lab.py. Unlike Tasks 1 and 2, these are ordinary functions, not methods.
def x_coordinates(points: list[data.Point]) -> list[float]:Return a new list containing the x attribute of each point, in the original order.
Example:
points = [
data.Point(1.0, 2.0),
data.Point(3.0, 4.0),
data.Point(-2.0, 5.0),
]
# x_coordinates(points) returns [1.0, 3.0, -2.0]Do not modify the input list or any input point. This is a map pattern.
def are_in_positive_quadrant(points: list[data.Point]) -> list[data.Point]:Return a new list of the existing points whose x and y attributes are both greater than zero. Preserve their order. Points lying on an axis do not qualify.
Example:
first = data.Point(1, 2)
second = data.Point(-3, 4)
third = data.Point(5, 6)
points = [first, second, third]
result = lab.are_in_positive_quadrant(points)
# result contains references to first and third, in that order.Do not construct new Point objects to fill the result list. This is a filter pattern.
Write multiple tests for each function in lab_tests.py. Include empty lists, one-element lists, negative values, and zero coordinates. For the filter, confirm that the returned entries are the same objects as those in the input using self.assertIs(...). Also confirm that the input list and its points remain unchanged.
- Why does the map produce numbers while the filter produces
Pointreferences? - How does a new result list differ from creating new point objects?
- Why does a point on an axis fail the first-quadrant test?
A path has a name and an ordered list of Point objects. The PointPath constructor in data.py already initializes:
self.name = name
self.points = []Implement both of the following methods on PointPath.
def add_point(self, point: Point) -> None:Append the existing Point object to self.points. Do not create a copy. Do not return a value.
Example:
path = data.PointPath("Walk")
p = data.Point(0, 0)
path.add_point(p)
# path.points[0] is the same object as pdef total_length(self) -> float:Return the sum of Euclidean distances between consecutive points in the path. Reuse the Point.distance_to method you wrote in Task 1. Do not duplicate the distance formula.
For a path containing fewer than two points, return 0.0. The method must not change the path or any point.
Example:
path = data.PointPath("Walk")
path.add_point(data.Point(0, 0))
path.add_point(data.Point(3, 4))
path.add_point(data.Point(6, 4))
result = path.total_length()
# result is 8.0: 5.0 + 3.0Write multiple tests in data_tests.py for empty paths, one-point paths, and paths containing at least two segments. Verify:
- points remain in the order they were added;
add_pointstores the original object reference, usingself.assertIs(...);total_lengthcalls on a path produce the expected result;- calling
total_lengthdoes not change the path or its points.
- What does
selfrefer to duringpath.add_point(p)? - What does
self.pointscontain? - How can you access the x-coordinate of the first point with
path.points[0].x? - Why is
distance_topreferable to reimplementing the calculation?
For every task:
- Write multiple unit tests, not only one example test.
- Include representative and boundary cases.
- Test empty collections where applicable.
- Verify mutation only where the requirements permit it.
- Verify reference identity where the requirements specify keeping an existing object.
- Run the full test suite before demonstrating the lab.
| Function or method | Kind | Main concept |
|---|---|---|
Point.distance_to |
Query method | self, other object, no mutation |
Point.translate |
Mutating method | Object state and aliasing |
x_coordinates |
Function | Map over a list of objects |
are_in_positive_quadrant |
Function | Filter; preserve object references |
PointPath.add_point |
Mutating method | Object containing a list of objects |
PointPath.total_length |
Query method | Iterate over objects; reuse a method |
Show an instructor:
- the completed implementations in
data.pyandlab.py; - passing unit tests in
data_tests.pyandlab_tests.py; - a query method that does not modify either point;
- a mutating method with an aliasing test;
- a list-processing function that preserves the original point references; and
- a
PointPathstoring points and computing the length of a multi-segment path.
Be prepared to explain methods, self, query versus mutation, aliasing, identity, lists of objects, objects containing lists, and method reuse.
These exercises combined functions, methods, and compound data. The next programming exercise will apply your existing iteration and data-representation skills to searching and sorting.