Skip to content
academia.sh

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 Record class; a class attribute (kind) has been added to its body, and the source and format capabilities 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 Record class; 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.
  • kind in the class body is shared as 1 object across three instances and is not found in any of their dicts; name inside __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.
  • self is 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 through SubFormatter, binds SubFormatter to cls.

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.

Start typing to search.

↑↓ Esc navigate · open · close