Lesson 01 / 11
Class and Instance
An attribute written in the class body is shared as 1 object across three instances; one written in the instance body produces 3 separate objects. `self` is not a keyword — it is the instance carried by the bound method built at the moment of access.
Contents
The Data Structures and Functional Tools course counted a container’s cost and closed on a line: choosing a container and writing one are separate things. Every protocol measured so far worked on objects the language already gave — a list had a length, a tuple could be a key, a dict could be compared. None of it was a contract we wrote; each was a ready-made property of a chosen container.
This course sits on the other side of that line: we write the side that takes part in the
protocol. And writing something does not mean it actually answers a call. The course has
one question: who answers the call? The first lesson builds the smallest form of that
question. When a class is defined, two separate namespaces come into being — the class
itself, and every instance built from it. When an attribute is read, which of the two does
the answer come from, and where is that name self born?
Two Separate Namespaces
When a class statement runs, the interpreter runs the body once and writes the names
produced in the body into the class object’s dict. A class is an object that exists at
runtime; the methods, too, are ordinary functions sitting inside that dict.
Every instance, in turn, arrives with its own dict. When self.name = ... is written
inside __init__, the name is written not to the class but to that instance’s dict. When an
attribute is read, the lookup checks the instance’s dict first, then the class’s, then the
class’s ancestors. Whoever is found first gives the answer. The shared rig’s
answering_class function writes the class side of this lookup out: it says which class
first carries a name during resolution.
__init__’s name follows from this arrangement too. When an instance call is made, an empty
object is built first, and __init__ runs on that object afterward — so
__init__ does not produce an object, it fills the dict of an object that has already been
built. This is why it returns nothing — it has no object to return, it has an object it was
given.
The measurement’s assumptions:
- CD1 — The measured class is the shared rig’s
Recordclass; a class attribute (kind) has been added to its body, and thesourceandformatcapabilities are unchanged. - CD2 — Three instances are built from the same class with a single comprehension; the
only difference between them is the name given to
__init__. - CD3 — The “separate object” column compares the value seen by the first two instances
with
is; the identity number itself is not printed. - CD4 — The “in instance dict” column counts how many instances carry the name in their own dict; names coming from the class do not count toward this number.
"""Where an attribute stands: the class body versus the instance body.""" class Record: """The shared rig's base class; two capabilities are used here.""" kind = "record" # written in the class body def __init__(self, name): self.name = name # written in the instance body def source(self): return "Record" def format(self): return f"<{self.source()}>" def answering_class(cls, name): """Which class provides an attribute: the first one found during resolution.""" for s in cls.__mro__: if name in s.__dict__: return s.__name__ return None trio = [Record(f"r{i}") for i in range(3)] print(f"{'attribute':<10s} {'answering':>11s} {'separate object':>16s} " f"{'in instance dict':>17s}") for name in ("kind", "name"): separate = getattr(trio[0], name) is not getattr(trio[1], name) local = sum(1 for t in trio if name in t.__dict__) print(f"{name:<10s} {answering_class(Record, name) or 'instance':>11s} {str(separate):>16s} " f"{local:>17d}") a, b = Record("a"), Record("b") print(f"\n{'at start':<19s}a.kind={a.kind!r} b.kind={b.kind!r} Record.kind={Record.kind!r} " f"kind in a's dict -> {'kind' in a.__dict__}") a.kind = "custom" print(f"{'a.kind assigned':<19s}a.kind={a.kind!r} b.kind={b.kind!r} Record.kind={Record.kind!r} " f"kind in a's dict -> {'kind' in a.__dict__}") del a.kind print(f"{'attribute deleted':<19s}a.kind={a.kind!r} b.kind={b.kind!r} Record.kind={Record.kind!r} " f"kind in a's dict -> {'kind' in a.__dict__}")
attribute answering separate object in instance dict kind Record False 0 name instance True 3 at start a.kind='record' b.kind='record' Record.kind='record' kind in a's dict -> False a.kind assigned a.kind='custom' b.kind='record' Record.kind='record' kind in a's dict -> True attribute deleted a.kind='record' b.kind='record' Record.kind='record' kind in a's dict -> False
The first table shows the two names standing in two separate places. When kind is read,
the answer comes from Record; all three instances see the same object, and none of
them carries this name in its own dict — the count is 0. When name is read, no class
answers; all three instances carry it in their own dict, the count is 3, and the objects
they see are separate.
The bottom section is a direct consequence of lookup order. a.kind = "custom" does not
change the name on the class; it opens a new entry in a‘s dict, and since that entry is
found first, it shadows the class’s. b and the class stay exactly as they were. Once
the instance’s name is deleted, the shadow lifts and a.kind reads the class’s value again.
Assigning to an instance never
touches the class — the only way to change the class is to write to the class.
The Class Body Runs Once
The distinction above has a cost side, and it goes unnoticed in careless writing: the class
body runs once, at definition time; the instance body (__init__) runs on every
instance. Writing = [] in the class body builds a single list, and every instance shares
that same list.
- CD5 — The measured class deliberately defines two names of the same kind in two separate bodies; there is no difference between them other than the names.
- CD6 — Three instances are built, and each one appends its own index to both containers; what is measured is how many items each instance sees at the end.
- CD7 — Sharing is shown with
is.
class Shared: """Defines two names of the same kind in separate bodies: one in the class, one in the instance.""" shared_box = [] # class body: runs once def __init__(self): self.own_box = [] # instance body: runs per instance trio = [Shared() for _ in range(3)] for i, n in enumerate(trio): n.shared_box.append(i) n.own_box.append(i) print(f"\n{'attribute':<14s} {'containers made':>16s} {'each instance sees':>20s} " f"{'shared':>13s}") print(f"{'shared_box':<14s} {1:16d} {str([len(n.shared_box) for n in trio]):>20s} " f"{str(trio[0].shared_box is trio[2].shared_box):>13s}") print(f"{'own_box':<14s} {3:16d} {str([len(n.own_box) for n in trio]):>20s} " f"{str(trio[0].own_box is trio[2].own_box):>13s}")
attribute containers made each instance sees shared shared_box 1 [3, 3, 3] True own_box 3 [1, 1, 1] False
Three instances each made one append to a container. The class body’s container was built 1 time, so all three appends went into the same list, and every instance sees 3 items. The instance body’s container was built 3 times, and each instance sees only what it appended itself — 1 item. The numbers come from the same rows; the only thing that separates them is where the body sits.
A writing rule follows from this: a mutable value is not written in the class body. The
class body is for constants, numbers, and immutable values — for those, sharing has no
observable consequence, because no one can mutate them in place. Any state that belongs to
one instance is set up inside __init__. The correct use of a class attribute shows up in
the same table: kind in the first measurement is a default, and an instance only
shadows it when needed; one object is enough for three instances, because nobody mutates it
in place.
self Is Not a Keyword
An object sitting in the class dict is an ordinary function, and its first parameter is no different from any other name. When accessed through an instance, a step is inserted: the access produces a bound method object that holds both the function and the instance together. This is why we do not write the first argument in the call — that argument is already bound at the moment of access.
- CD8 — The measurement continues with the first block’s
Recordclass; no new definition is made. - CD9 — The same call is made in two forms: through the instance, and through the class with the instance passed by hand. What is measured is whether the two forms give the same result.
- CD10 — The objects’ type is printed as a name; identity numbers and memory data are not printed.
- CD11 — The call in the last line is deliberately made without an instance; the exception’s name is recorded.
k = Record("k") print(f"\nk.format() -> {k.format()} | Record.format(k) -> {Record.format(k)} | " f"equal -> {k.format() == Record.format(k)}") print(f"type of object in class dict -> " f"{type(Record.__dict__['format']).__name__}") print(f"type of object accessed via instance -> {type(k.format).__name__}") print(f"bound method holds instance k -> {k.format.__self__ is k}") print(f"k.format is k.format -> {k.format is k.format} | " f"Record.format is Record.format -> {Record.format is Record.format}") print(f"function wrapped by bound method -> {k.format.__func__ is Record.format}") try: Record.format() except TypeError as e: print(f"call without instance -> {type(e).__name__}") print() print(f"{'capability':<10s} {'answering class':>17s}") for name in ("source", "format", "__init__"): print(f"{name:<10s} {answering_class(Record, name):>17s}") print(f"{'name':<10s} {answering_class(Record, 'name') or '—':>17s}")
k.format() -> <Record> | Record.format(k) -> <Record> | equal -> True type of object in class dict -> function type of object accessed via instance -> method bound method holds instance k -> True k.format is k.format -> False | Record.format is Record.format -> True function wrapped by bound method -> True call without instance -> TypeError capability answering class source Record format Record __init__ Record name —
The first line shows the two forms doing the same job: k.format() gives the same result as
Record.format(k). The second is the unfolded form of the first. The next two lines name
the step in between — the object sitting in the class dict is a function, the object
accessed through the instance is a method. The fourth line says what the method carries:
the instance the access was made through.
The fifth line proves that binding happens at the moment of access. Reading the same
name twice produces two separate bound-method objects; the is comparison gives False.
Accessing through the class involves no binding — the same function in the dict comes back
every time, and the comparison gives True. The sixth line ties the two together: the
function wrapped by the bound method is the very same function read from the class. This
is also why the instance-less call raises TypeError — the function expected a parameter,
and there was no instance to bind it.
This has two everyday consequences. First, a bound method carries as a value without
being called: passing k.format somewhere carries k with it, since the object holds the
instance inside itself — putting a method into a callback list puts the instance in too.
Second, a method can always be read off the class and called by hand; the instance is not a
hidden argument, it is an explicit, writable first one.
Object-oriented programming’s concepts were built in the Programming Fundamentals course;
they are not repeated here. There, a class was a template and an instance one production
of it. What is measured here is that the template itself is also an object, and which
dict the answer comes from. The last table closes this: Record answers all four names,
and no class answers the name attribute — it is not written in any class, it is born
separately in every instance.
Three Forms of Binding
Since binding is a rule, it can be changed. Three separate binding forms can be defined in the same class body: a method whose first argument is bound to the instance, one bound to the class, and one bound to nothing. All three sit in the same body and are called the same way; the only place they differ is what the first argument is at call time.
- CD12 — Three methods are defined in a single class body, and all three do the same job: return the name of whatever is bound to them. If nothing is bound, a dash is returned in its place.
- CD13 — The measurement is made through a subclass; the class that writes the bodies and the class the call is made on are deliberately separated.
- CD14 — The “call” columns call the method with no arguments; a call that cannot be satisfied falls with an exception, and the exception’s name is recorded with an exclamation prefix.
- CD15 — The access columns print the returned object’s type as a name; identity numbers are not printed.
class Formatter: """Three binding forms in the same body: bound to the instance, the class, and nothing.""" def instance_method(self): return type(self).__name__ @classmethod def class_method(cls): return cls.__name__ @staticmethod def static_method(): return "—" class SubFormatter(Formatter): pass def attempt(call): try: return call() except TypeError as e: return f"!{type(e).__name__}" n = SubFormatter() print(f"\n{'method':<15s} {'class access':>15s} {'instance access':>15s} " f"{'class call':>15s} {'instance call':>15s}") for name in ("instance_method", "class_method", "static_method"): print(f"{name:<15s} {type(getattr(SubFormatter, name)).__name__:>15s} " f"{type(getattr(n, name)).__name__:>15s} " f"{attempt(getattr(SubFormatter, name)):>15s} " f"{attempt(getattr(n, name)):>15s}") print(f"\nclass that wrote the body {answering_class(SubFormatter, 'class_method')}, " f"class bound to cls {SubFormatter.class_method()}")
method class access instance access class call instance call instance_method function method !TypeError SubFormatter class_method method method SubFormatter SubFormatter static_method function function — — class that wrote the body Formatter, class bound to cls SubFormatter
The table shows row by row where binding happens. The instance method is bound only when
accessed through the instance; the object accessed through the class is a bare function,
and calling it with no arguments falls with TypeError. The class method is bound on
both kinds of access — even read through the class it comes back as a method, because
the thing to bind is already the class itself; the no-argument call works either way. The
static method is bound on neither access; a bare function comes back both ways, and both
calls report that there is nothing bound.
Five of the six calls land, one falls. The falling call is not a gap, it is the rule itself: an instance method cannot be called without an instance, because there is nothing to fill its first parameter.
The last line is the first sharp form of the course’s question. class_method’s body is
written in Formatter; but because the call is made through SubFormatter, the class
bound to cls is SubFormatter. The class that writes the body and the context that
answers the call are not the same thing — the body sits in one place, the binding happens at
call time. What the next lesson measures is this same distinction, enlarged.
Summary
- Class and instance are two separate namespaces; attribute lookup checks the instance’s dict first, then the class’s, and whoever is found first gives the answer.
kindin the class body is shared as 1 object across three instances and is not found in any of their dicts;nameinside__init__produces 3 separate objects and sits in all three dicts.- Assigning to an instance does not change the class, it shadows it; once the instance’s name is deleted, the class’s value becomes visible again.
- The class body runs once, the instance body runs per instance: a container defined in the class is built 1 time and holds all 3 appends together; one defined in the instance is built 3 times.
selfis not a keyword: access through an instance produces a new bound method every time, that method carries the instance it was accessed through, and passes it as the first argument at call time.- There are three forms of binding; 5 of six calls land, and the one that falls is an
instance method called without an instance. A class method whose body is written in
Formatter, when called throughSubFormatter, bindsSubFormattertocls.
Next Step
In this lesson the one that always answered was Record, because there was only one class
and lookup ended on the first step. When classes are derived from each other, lookup gets
longer: a name may be written in more than one class, and resolution order decides which
one is found. The next lesson measures that order and pays off the course’s second claim —
across an inheritance tree, which classes does the body of four capabilities spread across;
how many of the same four capabilities does a class built with composition write in its own
body; and why does a method whose body is written in one class print another class’s name?
To keep your progress and take notes, Log in
My notes
Log in to take notes.