Grade by Grade
A. Programming Concepts and Skills · Data Types, Files and Expressions

Arrays of Objects: State, Behaviour and References

A1.5 A1.3 A4.3About 75 minutes
Learning goals
  • I can write a simple class with an initializer, fields and methods, and document it.
  • I can create and process a one-dimensional list of objects by calling methods on each element.
  • I can explain aliasing, shallow copies and deep copies of a list of objects.
  • I can explain how ==, is and in behave on objects and how to define equality by content.

Words to know

object
A value that bundles state (fields) and behaviour (methods), made from a class.
method
A function defined inside a class that works on one object, called with dot notation: toque.sell(5).
`self`
The first parameter of a method: the object the method was called on.
`__init__`
The initializer method that sets up a new object's fields when it is created.
alias
A second name for the same object. Changes through one name are seen through the other.
shallow copy
A new list containing the same references as the old one: the objects themselves are shared.
deep copy
A copy of a structure and every object inside it, made with copy.deepcopy.
null reference
A reference that points to no object (null in Java, None in Python).

Records that can do things

In Lesson 7, a record was just data: a name, a number, some goals. Real programs also need data that behaves. A store item should refuse to sell more than it has. A bank account should refuse a withdrawal that would go below zero. A bus in a simulation should move along its route.

An object bundles state (fields) with behaviour (methods). In this lesson you will write a small class, keep a list of its objects, and process the list by asking each object to do things. You will also learn the most common bug with lists of objects: two names that secretly refer to the same thing.

A class with behaviour

The class below models an item in the school store. __init__ sets up the fields. Each method receives self, the object it was called on, so toque.sell(5) runs sell with self being toque.

The important design idea: the class protects its own rules. Code outside the class asks the item to sell; it never subtracts from stock itself. That way the rule "stock is never negative" is checked in exactly one place. This is the beginning of encapsulation, which the sibling unit Modular and Object-Oriented Design develops fully.

class Item:
"""A product in the school store."""
def __init__(self, name, price, stock):
self.name = name
self.price = price # dollars
self.stock = stock # units on hand, never negative
def sell(self, qty):
"""Remove qty units if available; return True if the sale happened."""
if 0 < qty <= self.stock:
self.stock -= qty
return True
return False
def restock(self, qty):
"""Add qty (> 0) units."""
self.stock += qty
def value(self):
"""Return the value of the stock on hand."""
return self.price * self.stock
def __str__(self):
return f"{self.name}: {self.stock} @ ${self.price:.2f}"
toque = Item("Toque", 15.00, 12)
print(toque.sell(5), toque.stock)
toque.restock(10)
print(toque.sell(20), toque.stock)
print(toque)

A class with behaviour: the methods keep the rule "stock is never negative" true. __str__ controls how print shows the object. Output, line by line: True 7 | False 17 | Toque: 17 @ $15.00

Item("Toque",15.0,12)stock = 12sell(5)stock = 7restock(10)stock = 17sell(20)refused: 17the method checks the rule before changing the object
An object's state changes only through its methods. sell(20) is refused because only 17 toques are in stock, so the object never reaches an impossible state (negative stock).

A list of objects

A one-dimensional list of objects works just like a list of records, plus you can call methods on each element:

  • Process every object: for it in store: it.restock(5).
  • Search: compare a field, then call a method on the match.
  • Aggregate: sum(it.value() for it in store).
  • Filter: [it for it in store if it.stock < 5].

Notice how short the store program is. The details of selling are inside Item, so the main program reads like a description of the task.

class Item:
"""A product in the school store."""
def __init__(self, name, price, stock):
self.name = name
self.price = price # dollars
self.stock = stock # units on hand, never negative
def sell(self, qty):
"""Remove qty units if available; return True if the sale happened."""
if 0 < qty <= self.stock:
self.stock -= qty
return True
return False
def restock(self, qty):
"""Add qty (> 0) units."""
self.stock += qty
def value(self):
"""Return the value of the stock on hand."""
return self.price * self.stock
def __str__(self):
return f"{self.name}: {self.stock} @ ${self.price:.2f}"
store = [Item("Toque", 15.00, 17), Item("Mitts", 12.50, 4),
Item("Scarf", 18.00, 0), Item("Water bottle", 9.75, 30)]
orders = [("Mitts", 3), ("Scarf", 1), ("Toque", 2), ("Hoodie", 1)]
for name, qty in orders:
match = next((it for it in store if it.name == name), None)
if match is None:
print("No such item:", name)
elif not match.sell(qty):
print("Not enough", name)
for it in store:
print(it)
print(f"Total value: ${sum(it.value() for it in store):.2f}")
low = [it.name for it in store if it.stock < 5]
print("Reorder:", low)

A one-dimensional list of objects. The loop finds each ordered item with a linear search (next returns the first match or None) and asks the object to sell itself. The store code never changes stock directly. Output, line by line: Not enough Scarf | No such item: Hoodie | Toque: 15 @ $15.00 | Mitts: 1 @ $12.50 | Scarf: 0 @ $18.00 | Water bottle: 30 @ $9.75 | Total value: $530.00 | Reorder: ['Mitts', 'Scarf']

References, aliases and copies

A list of objects holds references (Lesson 7). That has three consequences:

  1. alias = shelf copies the reference to the list. Both names refer to one list.
  2. list(shelf), shelf[:] and shelf.copy() make a shallow copy: a new list, but the same item objects inside.
  3. copy.deepcopy(shelf) makes a deep copy: a new list and new copies of every object.

Choose deliberately. A shallow copy is right when you want a differently sorted view of the same objects. A deep copy is right when you want an independent "what if" version, such as trying out a set of sales without changing the real store.

shelfalias = shelf(alias: same list)copy1list(shelf)(new list, same items)ToqueMittscopy2copy.deepcopy(shelf)(new list, new items)ToqueMittsa change to a shared Toque shows through both listsfully independent
Copying a list of objects. A shallow copy (list(shelf) or shelf[:]) makes a new list that still refers to the same item objects. A deep copy copies the objects too. Plain assignment (alias = shelf) does not copy anything at all.
class Item:
"""A product in the school store."""
def __init__(self, name, price, stock):
self.name = name
self.price = price # dollars
self.stock = stock # units on hand, never negative
def sell(self, qty):
"""Remove qty units if available; return True if the sale happened."""
if 0 < qty <= self.stock:
self.stock -= qty
return True
return False
def restock(self, qty):
"""Add qty (> 0) units."""
self.stock += qty
def value(self):
"""Return the value of the stock on hand."""
return self.price * self.stock
def __str__(self):
return f"{self.name}: {self.stock} @ ${self.price:.2f}"
import copy
shelf = [Item("Toque", 15.00, 17), Item("Mitts", 12.50, 4)]
alias = shelf
copy1 = list(shelf)
copy2 = copy.deepcopy(shelf)
shelf[0].sell(10)
print(alias[0].stock, copy1[0].stock, copy2[0].stock)
copy1.append(Item("Scarf", 18.00, 2))
print(len(shelf), len(alias), len(copy1))
print(shelf[0] is copy1[0], shelf[0] is copy2[0])

Aliases and copies. Selling 10 toques through shelf shows up through alias and the shallow copy1, but not the deep copy2. Appending to copy1 does not change shelf, because copy1 is a different list. Output, line by line: 7 7 17 | 2 2 3 | True False

Worked example

Example 1: Equality of objects

Two Item objects are created with exactly the same name, price and stock: a = Item("Toque", 15.00, 12) and b = Item("Toque", 15.00, 12). What is a == b? What is a in [b]? How would you make equal-looking items compare equal?

  1. For an ordinary class, Python's default == compares identity: are these the same object? They are not, so a == b is False.
  2. in uses == for each element, so a in [b] is also False. This surprises people searching a list of objects.
  3. Fix 1: define __eq__(self, other) in the class to compare the fields that matter, for example return isinstance(other, Item) and self.name == other.name.
  4. Fix 2: write the class as a @dataclass (Lesson 7), which generates __eq__ comparing all fields.
  5. Java has the same rule: == compares references, and a class must override equals (and hashCode) to compare contents. The lesson Making Objects Comparable goes further, to ordering with <.

Both are False by default. Define __eq__ or use a dataclass to compare by content.

Document every class (A4.3)

Give every class and method a docstring: one line saying what it represents or does, plus any rules it keeps (here: stock is never negative). Comments on fields should state units (# dollars). The lesson Documenting Code to Industry Standards covers the full conventions.

Worked example

Example 2: An array of objects in Java versus Python

In Java, Item[] shelf = new Item[3]; creates an array. A student then writes shelf[0].sell(1); and the program crashes. Why, and what is the Python equivalent of the mistake?

  1. A new Java array of objects holds three null references: no Item objects exist yet. Calling a method on null throws a NullPointerException.
  2. Each element must be created: shelf[0] = new Item("Toque", 15.00, 12);.
  3. Python lists grow as needed, so you usually append real objects. The same bug appears if you pre-fill with None: shelf = [None] * 3 then shelf[0].sell(1) raises AttributeError: 'NoneType' object has no attribute 'sell'.
  4. Lesson: an array of objects is an array of references. Make sure every reference points to a real object before you use it.

The array held null references. Create each object before calling its methods; in Python avoid pre-filling with None.

Just for fun

Why did the two objects with identical fields refuse to admit they were equal?

Nobody had taught them __eq__, so they only recognized themselves.

Hands-on activity

Simulate your kitchen pantry

You need: Python and about 8 items from your pantry or fridge (cans, boxes, fruit).

  1. Write a class PantryItem with fields name, quantity and unit (such as "cans" or "g"), and methods use(amount) (refuse if there is not enough), add(amount) and __str__.
  2. Make a list of objects for your real items, with real quantities.
  3. Write a list of uses for a recipe you might make this week, and process it like the store orders, printing a message for anything missing or short.
  4. Make a deep copy of the pantry, run the recipe on the copy, and show that the original is unchanged. Then show what goes wrong if you use a shallow copy instead.

What to look for: the class refuses impossible uses, the main loop calls methods instead of changing fields directly, and a clear explanation of the shallow-copy result.

Your turn

1

Using the Item class, what is printed? x = Item("Mitts", 12.50, 4) then print(x.sell(4), x.sell(1), x.stock)

2

Write one line that restocks every item in store by 5.

3

Write an expression for the number of items in store with zero stock.

4

After a = store and a.append(Item("Hoodie", 40.00, 3)), how many items does store have if it had 4 before? Why?

5

After b = store[:] and b.append(...), does store change? After b[0].sell(1), does store[0] change?

6

Why is it better to call toque.sell(3) than to write toque.stock -= 3 in the main program?

7

Two separately created Item objects have identical fields. What does a == b give for the Item class in this lesson?

More practice: warm-up

1

What is the first parameter of every method?

2

Which method sets up a new object's fields?

3

Which function makes a deep copy?

4

What does print(obj) call to get its text?

5

In Java, what does a new array of objects contain before you fill it?

More practice: on your own

1

Add a method discount(self, percent) to Item that lowers the price by that percent, refusing values outside 0 to 100. Show the call that takes 25% off a $15.00 toque.

2

Write code that finds the item with the highest stock value (price × stock) in store.

3

Explain the output of print(shelf[0] is copy1[0], shelf[0] is copy2[0]) in the copy program.

4

Write __eq__ for Item so that two items are equal when their names match, ignoring case.

5

A student writes cart = [Item("Toque", 15.00, 0)] * 3 to create three empty items. Explain the bug and fix it.

Challenge

1

Write transfer(store, from_name, to_name, qty) for moving stock between two items (for example between two sizes) so that either both changes happen or neither does.

2

A list of 1000 Item objects is sorted with sorted(store, key=lambda it: it.price). How many Item objects exist afterwards: 1000 or 2000? Explain.

3

Explain why copy.deepcopy can be slow or even wrong for some objects, and when a shallow copy is the better design.

Check your understanding

Quiz

1.

What does self refer to inside toque.sell(5)?

2.

After b = a where a is a list of objects, then b.append(x), what happens to a?

3.

c = a[:] (a shallow copy). Then c[0].stock = 0. What is a[0].stock?

4.

Two separately created objects of an ordinary class have equal fields. What is x == y by default?

5.

Why should the main program call item.sell(n) instead of changing item.stock directly?

6.

In Java, Item[] a = new Item[2]; a[0].sell(1); causes which error?

For parents and teachers

Big idea: A1.5 names arrays of objects explicitly. Students also practise documenting classes (A4.3) and comparing objects (A1.3).

How to help: the pantry activity makes references concrete. Ask: "If I write the shopping list on the fridge and you take a photo of it, is that a deep copy or an alias?" (A photo is a copy; pointing at the fridge list is an alias.)

Common difficulties:

  • Forgetting self in a method definition or in front of a field (stock instead of self.stock).
  • Thinking b = a copies a list.
  • Expecting == to compare objects' contents without __eq__.
  • Changing fields directly from outside the class and bypassing its rules.