Classes and shared state
Organize a small program into modules and classes while handling files, failures and iteration.
THE IDEA
A class is a blueprint, an instance is a thing
A class bundles data and the functions that work on it. Calling the class, Inventory(5), creates an instance: one particular object. The special method __init__ runs on the new object, and inside it self is that object. Writing self.count = count stores a value on that instance.
class Inventory:
def __init__(self, count):
self.count = count # belongs to this one object
def reserve(self, n):
self.count -= n
first, second = Inventory(5), Inventory(5)
first.reserve(2)
print(first.count, second.count) # 3 5Two objects, two separate count values. That independence is the point of an instance.
Where does an attribute live?
A name assigned in the class body belongs to the class. A name assigned on self belongs to the instance. When you read obj.name, Python looks on the object first, and if it is not there, on the class. That fall-through is useful for constants, and dangerous for data that should be private to each object.
Program
class Bag: items = []a = Bag()b = Bag()a.items.append('x')print(b.items)print(a.items is b.items)Objects in memory
Printed
The class statement ran. The class object now exists and owns the attributes written in its body.
The shared mutable class attribute bug
In the buggy version, a.items.append("x") does not give a a new attribute. It finds the class's list and changes that list in place. Every instance that falls through to the class sees the change. The fix is to create the list per instance in __init__.
Predict first. Two bags use the buggy class (items = [] in the class body). a.items.append("x") runs. What does b.items show?
Predict first. With count = 0 on the class, a.count += 1 runs. What is b.count?
Sharing on purpose
Chapter context
As a program grows, the difficult part becomes keeping state and responsibilities understandable. Use simple functions for transformations, classes for related state and behavior, and modules to give those responsibilities names.
TRY IT YOURSELF
Exercise 1: fix the shared inventory
Inventoryhas a bug:log = []sits in the class body, so every Inventory shares one list. Give each object its own list by creatingself.login__init__, and remove the class-levellog.- Write
reserve(self, n). Ifnis more thanself.count, raiseValueError("insufficient stock")and change nothing. Otherwise subtractnfromself.countand appendntoself.log. - Two Inventory objects must never affect each other.
Exercise 2: shared state on purpose
- Sometimes sharing is exactly what you want.
Ticket.createdis a class attribute that counts how many tickets exist. Each ticket must also get its **own** number: 1 for the first ticket, 2 for the second and so on. - In
__init__, add 1 to the counter **on the class** (writeTicket.created += 1), then storeself.number = Ticket.created. - Careful:
self.created += 1looks similar, but it reads the class value and then creates a brand new attribute on the object. The class counter would never move.
Both exercises must be solved to finish this lesson.
Stuck, or something not working? You can come back to it any time.
CHECK YOUR UNDERSTANDING
Two bags share class-level items = []; one appends 'a'. What does the other see?
Chapter reference: math, worked example & code
This section belongs to the whole chapter, so it is the same on every lesson in it and may cover a different lesson's topic. The code for the experiment you just ran is inside the experiment itself.
Math and notation
Reason about state ownership before adding abstraction. For a transformation f, repeated calls should have independent results unless shared state is explicitly intended. A lazy iterator retains only the state required for its next step; it does not imply that downstream consumers also use constant memory.
Worked example
An inventory starts with 5 items. reserve(2) leaves 3. A second independent inventory still contains 5. If both objects instead refer to the same list, mutating one changes what both observe. Draw object references as arrows to make this distinction explicit.
Reference code
The following example uses the Python standard library and does not call a remote service.
class Inventory:
def __init__(self, count):
self.count = count
def reserve(self, n):
if n > self.count:
raise ValueError("insufficient stock")
self.count -= n
first, second = Inventory(5), Inventory(5)
first.reserve(2)
assert (first.count, second.count) == (3, 5)
Python examples are provided for study and local execution; this page does not execute Python.
Common mistakes
Type annotations are not validation. Catching Exception and continuing can hide corrupt state. Lazy iteration does not make a later list(...) memory-free.