Modeling Systems

Methods and Valid State


Learning Objectives

  • You can define methods that read and change one instance’s state.
  • You can distinguish a returned result from a state change.
  • You can establish valid initial state and preserve rules across operations.
  • You can test successful and rejected operations without tying them to user interaction.

The objects in the previous chapter stored information. A function can use that information to answer a question. A method gives the function a home on the class whose instances it uses.

A Method Uses the Instance’s State

Consider a small study session and a function that checks its length:

class StudySession:
def __init__(self, subject, minutes):
self.subject = subject
self.minutes = minutes
def is_long(session):
return session.minutes >= 60
session = StudySession("History", 30)
print(is_long(session)) # False

The function receives an object and reads its minutes attribute. To make the same operation a method, move the function inside the class and call its first parameter self:

No files opened Select a file to edit

This prints False and then True. In short.is_long(), Python supplies short as the self argument. The next call supplies long. One method definition can therefore answer the question for either instance.

For these ordinary methods, short.is_long() corresponds to StudySession.is_long(short). The object-method notation is the usual way to write the call. The equivalence helps explain where self comes from.

Neither call changes a session. The method reads the current state and returns a result. Practise that distinction in the following exercise.

Price a delivery

0 / 10 points

Complete shipping_cost(self) in the supplied Package class. A package has a destination string, a nonnegative weight in kilograms, and a boolean priority flag. Weights may be integers or fractional numbers. All supplied values are valid.

The ordinary cost is 5 + weight * 1.5. Priority delivery adds 10 to that cost. Return the cost as a number without rounding or printing it.

Calculate from the package’s current attributes each time. Calling the method must leave those attributes unchanged. Different packages must give results based on their own values. Keep the supplied initializer and is_heavy method working.

ordinary = Package("North depot", 2, False)
priority = Package("East depot", 2, True)
print(ordinary.shipping_cost()) # 8.0
print(priority.shipping_cost()) # 18.0
print(ordinary.weight) # 2

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.

A Method Can Also Change State

The LoanItem from the previous chapter can gain operations too. An available item can be borrowed. An item already on loan cannot be borrowed again. Returning an item changes its availability back, but only if it was actually on loan.

This version still assumes valid nonempty text fields. Focus first on what the methods return and change:

No files opened Select a file to edit

Before stepping, predict the first call’s returned result and the value of available after it. Should those booleans be equal?

The stepper uses the same borrowing rules, with only availability shown. It leaves out the item’s text fields so you can follow one attribute, each method’s self, and the values returned to print.

Code

Step 1 of 18

1 / 18
Formatting...

State

main

No variables

Objects

No objects yet

Output

No output yet

The output is True, False, and True. Every call’s self refers to the same item, so each call sees the state left by the previous one. The rejected second borrow leaves that state unchanged.

Add print(camera.available) after each call in the editor to compare the returned result with the stored state.

Returned Results and Changed State Are Separate

borrow() returns a boolean describing whether this operation succeeded. It also changes available when successful.

Those are two different observable effects. The returned True is not the new value of available; a successful borrow makes availability False.

A caller can decide how to present the result:

camera = LoanItem("E10", "Camera", "media")
if camera.borrow():
print("Item borrowed.")
else:
print("Item could not be borrowed.")

The model is the code that represents the items and applies their rules. These methods belong to the model. The user interface receives user actions and presents results, such as the messages above. Keeping print and input outside these methods lets the same model serve a text interface, a graphical interface, or a test.

Initialization Establishes the Rule

So far, the caller has been responsible for providing valid fields. We can make initialization check that requirement. Replace LoanItem.__init__ above with this version, keeping the borrowing and returning methods:

def __init__(self, item_id, name, category):
for value in [item_id, name, category]:
if type(value) != str:
raise ValueError("Item fields must be text.")
if value.strip() == "":
raise ValueError("Item fields must not be empty.")
self.item_id = item_id.strip()
self.name = name.strip()
self.category = category.strip()
self.available = True

Keep this method indented inside LoanItem. It rejects invalid field values before assigning the attributes. A successful item starts with nonempty trimmed text and available == True.

A property that should remain true across the supported operations is often called an invariant. Here, availability remains a boolean, and the borrowing methods preserve the intended transition between available and on-loan states.

The class does not make arbitrary external assignment impossible. This would still bypass the method:

camera.available = "maybe"

It bypasses the borrowing rules. borrow() and return_item() are public methods: operations that callers are expected to use. Callers should use them when changing availability so those rules are applied.

For these exercises, inspect attributes when needed, but use the stated methods for borrowing and returning. Later design work can choose stronger conventions about which attributes callers should use.

Check an Update Before Changing State

Initialization checks the starting state. A later update must preserve the same rules. Consider a counter that may hold only nonnegative integers:

No files opened Select a file to edit

The method checks proposed before assigning to self.value. If it raises, the previous value is still stored. The same order matters when updating an item’s name.

Rename an item

0 / 15 points

Add rename(self, new_name) to the supplied LoanItem class. Require a string that remains nonempty after removing surrounding whitespace. On success, store the stripped name and return None. Preserve internal spaces. On invalid input, raise ValueError and leave the old name unchanged. Error wording is your choice. Every other attribute, including availability, must remain unchanged. Do not print or ask for input. Keep the other supplied methods’ behavior.

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.

Expected Refusals Are Not Always Exceptions

The class call LoanItem(...) raises ValueError when its initializer rejects invalid fields. The borrowing method returns False when the item is already borrowed.

That distinction is deliberate. An ordinary attempt to borrow an unavailable item is an expected outcome of the operation, not evidence that the program cannot represent the item at all.

Other APIs might choose a different contract. What matters is making the decision explicit and testing it consistently, rather than treating every unsuccessful operation in the same way.

Apply both kinds of outcome to a new object: invalid amounts are rejected, while a valid request may be refused because of the current state. Check the amount after each operation.

Manage a water tank

0 / 25 points

Define WaterTank(capacity). Capacity must be a positive integer; reject booleans and other types with ValueError. A new tank has capacity and amount attributes, with amount initially 0.

  • add(litres) accepts a nonnegative integer (not a boolean). If it would exceed capacity, return False without changing amount. Otherwise add it and return True.
  • use(litres) has the same argument rule. If it would use more than the current amount, return False without change. Otherwise subtract it and return True.

Invalid arguments raise ValueError without changing state. Zero is a valid no-change operation and returns True. Do not print or read input. Keep separate tanks 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. What supplies the self argument when camera.borrow() runs?
  2. Why does successful borrowing return True but store False in availability?
  3. Why should the model not print the success message itself?
  4. Which rule does initialization establish before ordinary methods can run?
  5. Why does checking before assignment matter for a rejected rename?