Modeling Systems

Objects Working Together


Learning Objectives

  • You can use lists of instances and return references to existing objects.
  • You can coordinate objects through composition and delegation.
  • You can assign rules to the object responsible for maintaining them.
  • You can test independent collections and deliberate sharing of contained objects.

A single item can manage borrowing and returning itself. A library needs to find items, prevent duplicate identifiers, and coordinate operations across a collection.

Those are different responsibilities. One object can use other objects without taking over every detail of their behavior.

Collections Can Contain Instances

Using LoanItem from the previous chapter:

items = [
LoanItem("E10", "Camera", "media"),
LoanItem("E20", "Tent", "outdoors")
]
for item in items:
print(item.name)

The loop receives one instance at a time. List iteration did not change because the values became custom objects.

A returned instance is also an ordinary object reference. If a search returns an item from this list, a caller’s method call on that item affects the same object the list contains.

Give the Collection Its Own Responsibility

Add this class after the existing LoanItem definition:

class EquipmentLibrary:
def __init__(self):
self._items = []
def find(self, item_id):
for item in self._items:
if item.item_id == item_id:
return item
return None

_items begins with an underscore to mark it as an implementation detail. The convention asks callers to use the library’s operations rather than modify its list directly. Python does not enforce that convention as an access restriction.

The list is created inside __init__, so each library instance gets its own collection.

None Means the Search Found No Item

The search returns either an existing instance or None. The tests use is None and is not None to ask whether the result is that specific absence marker.

is compares object identity, not equal contents. Use this idiom for None; do not replace ordinary string or numeric equality tests with identity comparisons.

The find contract uses an exact, already-trimmed identifier. User input can be stripped by the user interface before calling it. The library does not guess that "e10" and "E10" mean the same identifier.

Add Only After the Checks Succeed

Add this method inside EquipmentLibrary, alongside find:

def add_item(self, item_id, name, category):
item = LoanItem(item_id, name, category)
if self.find(item.item_id) is not None:
raise ValueError("The item identifier already exists.")
self._items.append(item)
return item

LoanItem checks and trims the text. The library can then check the identifier against the whole collection. It appends the new object only after both checks succeed.

Delegate the Rule to Its Owner

Next, add the borrowing operation inside the same class:

def borrow_item(self, item_id):
item = self.find(item_id)
if item is None:
return False
return item.borrow()

borrow_item finds the requested object. It does not duplicate the availability decision:

return item.borrow()

This is delegation: one object asks another object’s method to do part of the work. The item “owns” the borrowing rule in the sense that it is responsible for checking and applying it. Other parts of the program can still hold references to that item. If the rule changes later, the library need not contain a second copy to update.

The complete example below brings the two classes together. Predict which object changes when library.borrow_item("E10") runs, then run the assertions. A successful run prints Checks passed..

No files opened Select a file to edit

The identity assertion checks that searching returns the existing item, not a separately constructed replacement. The diagram below shows which objects the names refer to during the call to LoanItem.borrow.

The library’s list and the names camera, item, and self refer to the same loan-item instance.

Only one loan-item instance appears in the diagram. When borrow changes self.available, the change is visible through camera and through the library’s list. In EquipmentLibrary.borrow_item, self refers to the library; in LoanItem.borrow, it refers to the item receiving that call.

Before stepping through the calls, predict the two printed values. Which instance does self refer to in each method?

This reduced example keeps only the item’s identifier and availability. To keep setup short, its library initializer accepts an existing item; the complete example above uses add_item and validates the fields.

Code

Step 1 of 18

1 / 18
Formatting...

State

main

No variables

Objects

No objects yet

Output

No output yet

Pause when LoanItem.borrow changes available. The two active methods each have a local self, but those names refer to different objects. Follow the item’s True result back through borrow_item to the caller: the library coordinates the operation, while the item decides whether borrowing succeeds.

Change the identifier in the borrow assertion to "missing". Predict which assertion fails and why the camera stays available. Restore "E10" afterward.

The next exercise supplies these definitions and asks you to add the matching return operation.

Return library equipment

0 / 15 points

Complete EquipmentLibrary.return_item(item_id) in the supplied classes. For an exact identifier that exists, call that item’s return_item() and return its boolean result. For a missing identifier, return False without changing anything. Do not duplicate the item’s transition rule in the library. Identifiers are case-sensitive and already trimmed by the caller. Do not print or read input. Keep the supplied addition, search, and borrowing behavior. Check repeated returns and references to the original item as well as a successful return.

Work in main.py. Open Show file tree, select check.py, and Run to try the supplied sample checks. Add cases for the remaining requirements, including boundary values and repeated operations where relevant. Keep test calls in check.py so importing main.py does not print or ask for input. Submit sends the files for grading against the full contract and returns feedback.

Query a Collection Without Changing It

The list-building pattern from Part 2 also works with objects: start a new result list, inspect each object, and append a selected value when it meets the condition. Reading item.name or item.available does not change the item.

Decide what the caller should receive. A search returns the existing item instance, so calling its methods can change the item in the library. A list of names can instead be a separate list of strings. The next exercise asks for that second kind of result.

List library equipment

0 / 15 points

Complete EquipmentLibrary.available_names() in the supplied classes. Return a new list containing the names of all currently available items, in insertion order. Different items may share a name; keep both occurrences. Do not print or change the library or its items. An empty library or one with no available items returns []. Changing the returned list must not change the library. Use the supplied add_item, find, and borrowing methods in your checks.

Work in main.py. Open Show file tree, select check.py, and Run to try the supplied sample checks. Add cases for the remaining requirements, including boundary values and repeated operations where relevant. Keep test calls in check.py so importing main.py does not print or ask for input. Submit sends the files for grading against the full contract and returns feedback.

Objects Contain Other Objects

An EquipmentLibrary has a collection of LoanItem instances. This relationship is called composition: one object is built using other objects.

The library keeps the collection and coordinates operations. Each item keeps its own availability and decides whether borrowing succeeds. Containing an object lets the library use that object’s methods without copying its rules.

Use the same relationship in a study log: the log stores session objects and calculates a total from their current durations.

Collect study sessions

0 / 25 points

Complete ReadingLog using the supplied StudySession class. Each log starts with an independent empty sessions list.

  • add_session(subject, minutes) receives a subject string and positive integer minutes. Preserve the subject exactly, including whitespace or an empty string. Create and append a new StudySession, then return that instance. Repeated subjects are allowed.
  • total_minutes() returns the total of the durations currently stored in its sessions, or 0 for an empty log.

Do not print or read input. A caller may change the minutes on a returned session; subsequent totals must reflect that change. Separate logs must remain independent.

Work in main.py. Open Show file tree, select check.py, and Run to try the supplied sample checks. Add cases for the remaining requirements, including boundary values and repeated operations where relevant. Keep test calls in check.py so importing main.py does not print or ask for input. Submit sends the files for grading against the full contract and returns feedback.

Check Your Understanding

  1. Which rule belongs to the item and which belongs to the library?
  2. Why does find return an existing instance rather than a replacement?
  3. What does the underscore in _items communicate, and what does it not enforce?
  4. Why is the new list created inside __init__?
  5. How does a study log use the session objects it contains to calculate a total?