Skip to content
VLSI Mentor

Python Part I · Module 16 · Object-Oriented Programming

Instance Methods

So far the objects in this module have been containers: they hold four fields and do nothing. Adding behaviour is syntactically trivial — a function written inside the class body — and the interesting question is not how but why, because Chapter 16.3 already demonstrated that a method call is literally a function call with the object passed in. If the two are the same operation, choosing between them cannot be a technical decision. It is a decision about where a piece of knowledge belongs, and that turns out to matter a great deal more than the syntax does.

Intermediate15 min readPythonPart IFundamentalsOOPMethods

Part I · Module 16 · Chapter 16.5 · Instance Methods

1. A Function Inside the Class Body

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class TestRun:
    def __init__(self, name, seed):
        self.name = name
        self.seed = seed

    def label(self):
        return f"{self.name}/{self.seed}"


run = TestRun("smoke", 1234)
print(run.label())
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
smoke/1234

label is defined exactly like any function, with self as its first parameter (Chapter 16.3). It reads the object's own attributes through self and returns a value.

A method may take further parameters after self:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class TestRun:
    def __init__(self, name, seed):
        self.name = name
        self.seed = seed

    def path(self, root, extension=".log"):
        return f"{root}/{self.name}_{self.seed}{extension}"


run = TestRun("smoke", 1234)
print(run.path("runs"))
print(run.path("scratch", extension=".wlf"))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
runs/smoke_1234.log
scratch/smoke_1234.wlf

Defaults and keyword arguments work as Module 11 described. self is simply the first in the list.

2. Why Not Just Store the Value?

The obvious alternative is to compute the label once in __init__ and keep it as a field. The two versions behave identically until something changes:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class Stored:
    def __init__(self, name, seed):
        self.name = name
        self.seed = seed
        self.label = f"{name}/{seed}"


class Computed:
    def __init__(self, name, seed):
        self.name = name
        self.seed = seed

    def label(self):
        return f"{self.name}/{self.seed}"


a = Stored("smoke", 1234)
b = Computed("smoke", 1234)

print("initially   :", a.label, "|", b.label())

a.seed = 9999
b.seed = 9999

print("after change:", a.label, "|", b.label())
print("Stored  reports seed", a.seed, "with label", a.label)
print("Computed reports seed", b.seed, "with label", b.label())
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
initially   : smoke/1234 | smoke/1234
after change: smoke/1234 | smoke/9999
Stored  reports seed 9999 with label smoke/1234
Computed reports seed 9999 with label smoke/9999

Stored is now internally inconsistent. It reports seed 9999 and a label built from 1234, because the label was computed once at construction and the later assignment could not reach it. Nothing raised; any report printing both fields simply shows two different seeds for one run.

Computed cannot drift, because there is no stored copy to drift from — the label is derived at the moment it is asked for.

A derived value stored as a field can go stale. A method computing it on demand cannot.

That is the first real argument for behaviour living on the object. The cost is recomputation on every call, which for a formatted string is nothing; when it is genuinely expensive, caching is a deliberate decision rather than the default.

3. The Function Version Is Mechanically the Same

Now the canonical question. Here is label as a plain function:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class TestRun:
    def __init__(self, name, seed):
        self.name = name
        self.seed = seed

    def label(self):
        return f"{self.name}/{self.seed}"


def label_of(run):
    return f"{run.name}/{run.seed}"


run = TestRun("smoke", 1234)

print("method  :", run.label())
print("function:", label_of(run))
print("long form:", TestRun.label(run))
print("identical:", run.label() == label_of(run) == TestRun.label(run))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
method  : smoke/1234
function: smoke/1234
long form: smoke/1234
identical: True

Three spellings, one result. Chapter 16.3 Section 3 already established why: run.label() is TestRun.label(run). There is no performance difference and no access difference — label_of reads run.name just as freely as the method reads self.name. Python has no private data (Chapter 16.7).

So the choice is not technical.

4. What Actually Differs

Three things, none of them about mechanism:

Discoverability. run.<tab> in any editor lists label. label_of is findable only if you know it exists and where it lives. For a reader meeting your code, a method is attached to the thing it concerns.

Coupling. label_of depends on run having .name and .seed — the same dependency the method has, but written in a file that may be far away. When the field is renamed, the method is adjacent to the change and the function is not.

Polymorphism. This is the one that matters most and cannot be replicated:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class Job:
    def __init__(self, name):
        self.name = name

    def describe(self):
        return f"job {self.name}"


class SimulationJob(Job):
    def describe(self):
        return f"simulation {self.name}"


def describe_of(job):
    return f"job {job.name}"


for j in (Job("alu"), SimulationJob("smoke")):
    print(f"{type(j).__name__:<14} method={j.describe():<22} function={describe_of(j)}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
Job            method=job alu                function=job alu
SimulationJob  method=simulation smoke       function=job smoke

The method gave the right answer for both; the function gave the Job answer for both. A method is chosen by the object's class at call time, so a more specific class can supply a better version. A function cannot — it would need an explicit check of what it was handed.

That is Chapters 16.11 and 16.12's subject, previewed here because it is the one genuine, unreplicable difference.

5. When a Function Is the Right Answer

Methods are not always right. The test:

Does this operation belong to one object, using its internals? Or does it span several objects, or belong to the caller's concerns?

A summary over a list of results belongs to nobody:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class TestResult:
    def __init__(self, name, passed, runtime_s):
        self.name = name
        self.passed = passed
        self.runtime_s = runtime_s

    def label(self):
        return f"{self.name} {'PASS' if self.passed else 'FAIL'}"


def summarise(results):
    """Spans the whole collection -- not any one result's business."""
    total = 0.0
    failures = []
    for r in results:
        total += r.runtime_s
        if not r.passed:
            failures.append(r.name)
    return total, failures


RESULTS = [
    TestResult("smoke", True, 182.4),
    TestResult("cdc", False, 204.1),
    TestResult("burst", True, 199.8),
]

for r in RESULTS:
    print(" ", r.label())

total, failures = summarise(RESULTS)
print(f"total {total:.1f}s | failures: {failures}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  smoke PASS
  cdc FAIL
  burst PASS
total 586.3s | failures: ['cdc']

label() is a method because it is about one result. summarise() is a function because it is about the set, and making it TestResult.summarise would mean asking one result to describe its peers.

A rough rule: if the body would mostly say self, write a method. If it would mostly loop, write a function.

6. Methods Can Call Other Methods

Through self, like anything else:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class TestRun:
    def __init__(self, name, seed, runtime_s):
        self.name = name
        self.seed = seed
        self.runtime_s = runtime_s

    def label(self):
        return f"{self.name}/{self.seed}"

    def is_slow(self):
        return self.runtime_s > 200

    def report(self):
        flag = " SLOW" if self.is_slow() else ""
        return f"{self.label()}{flag}"


for r in (TestRun("smoke", 1234, 182.4), TestRun("cdc", 5678, 204.1)):
    print(r.report())
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
smoke/1234
cdc/5678 SLOW

report calls self.label() and self.is_slow(). The lookup happens at call time, on whatever object self is — which is the mechanism that makes Chapter 16.11's overriding work, because a subclass replacing is_slow changes what report does without report being touched.

7. Predict, Run, Observe

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class Counter:
    def __init__(self):
        self.value = 0

    def bump(self):
        self.value += 1

    def bumped_twice(self):
        self.bump()
        self.bump()
        return self.value


c = Counter()

print("A:", c.bump())
print("B:", c.value)
print("C:", c.bumped_twice())
print("D:", c.value)
print("E:", Counter().bumped_twice())
print("F:", c.value)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A: None
B: 1
C: 3
D: 3
E: 2
F: 3

A is None — bump has no return, so it returns None (Chapter 11.8). A method that mutates and returns nothing is a normal and deliberate shape, and Chapter 16.6 is about that choice.

C — bumped_twice called self.bump() twice on an object already at 1, giving 3.

E is 2, not 5. Counter() built a fresh object starting at 0. Each instance has its own value.

F confirms the fresh object did not disturb c.

8. Exercises

Exercise 1 — a first method. Add a method to a class that returns a string built from two of its attributes. Call it on two instances.

Exercise 2 — extra parameters. Write a method taking two arguments after self, one with a default. Call it three ways.

Exercise 3 — the stale field. Reproduce Section 2's Stored versus Computed. Change an attribute and show the two disagree.

Exercise 4 — three spellings. Call the same behaviour as a method, as a plain function, and in the Class.method(obj) long form. Confirm all three agree.

Exercise 5 — the one real difference. Reproduce Section 4's polymorphism example. Explain in two sentences why the function could not be fixed without an explicit type check.

Exercise 6 — method or function. For each, choose and justify: (a) a test's log path (b) the pass rate of a suite (c) whether one result is slow (d) the slowest result in a list (e) a result's human-readable label.

Exercise 7 — composition through self. Write a method that calls two other methods on the same object. Change one of the two and confirm the caller follows.

Exercise 8 — returning None. Write one method that mutates and returns nothing and one that returns a new value without mutating. Say which you would prefer for a rename operation and why.

9. Summary

  • A method is a function in the class body whose first parameter is self.
  • Methods take further parameters normally, including defaults and keyword arguments.
  • A derived value stored as a field goes stale when its inputs change; a method computing it cannot.
  • A method and a function taking the object are mechanically identical — obj.m() is Class.m(obj).
  • What differs is discoverability, coupling and polymorphism, not mechanism.
  • Polymorphism is the unreplicable one: a method is selected by the object's class at call time, so a subclass can supply a better version.
  • An operation spanning several objects belongs in a function, not on any one of them.
  • If the body would mostly say self, write a method; if it would mostly loop, write a function.
  • Methods call each other through self, resolved at call time.
  • A method with no return returns None, which is a normal shape for one that mutates.

The next chapter is about methods that change the object, and the decision of whether they should.

Where this fits

Part of the Python curriculum.