Foundations: Python, Git and toolsLoading progress…

Classes and shared state

Organize a small program into modules and classes while handling files, failures and iteration.

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 5

Two 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

class Bag
items = list#1 []

Printed

The class statement ran. The class object now exists and owns the attributes written in its body.

Run the program line by line. The orange boxes are list objects. In the buggy version, a and b never get a list of their own. Then switch to the second program and compare a number with a list.

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.

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.