B3.2.2 · Fundamentals of OOP for multiple classes (HL)
Polymorphism and method overriding
One instruction, sent to many different kinds of object, and each one answers it in the way that is right for itself.
Explainer
Kirkstall Print Works runs one queue for everything it prints: posters, stapled booklets, cut-out stickers. When the manager presses "quote this queue", every job has to produce a price, but no two job types are priced the same way. Posters go by sheet, booklets by page count times copies, stickers add a fixed die-cutting fee on top. The queue code should not have to know any of that. It should ask each job for its price and trust the job to answer correctly.
Polymorphism (from the Greek for "many forms") is the ability of objects of different classes to respond to the same method call, each in the way appropriate to its own class. The caller uses one name; which code actually runs depends on the object.
This is what makes the queue loop flexible and the code reusable: one loop handles every job type that exists today, and keeps working unchanged for every job type added later. Without it, that loop would be a growing chain of type checks that has to be edited every time the business offers something new.
Explainer
Two forms, decided at two different times
The syllabus asks for both. They share the "one name, several behaviours" idea, but they differ in when the decision about which code to run gets made.
Dynamic (runtime) polymorphism is achieved by method overriding. A child class provides its own version of a method that already exists in its parent, with the same name and the same parameters. Which version runs is decided while the program is running, based on what the object actually is.
Static (compile-time) polymorphism is achieved by method overloading. One class provides several methods sharing a name but taking different parameter lists. Which version runs is decided before the program ever runs, by matching the arguments at the call site to a signature.
Overriding: same name, same parameters, different class, chosen at runtime. Overloading: same name, different parameters, same class, chosen at compile time. Mixing these two up is the single most common error on this statement.
Worked example
Dynamic polymorphism: overriding estimate_cost()
Each job type replaces the parent's pricing method with its own. Note that every version has an identical signature, which is exactly what makes it overriding rather than overloading.
# the shared shape, and a fallback price class PrintJob: def __init__(self, ref, copies): self.ref = ref self.copies = copies def estimate_cost(self): return self.copies * 0.10 class PosterJob(PrintJob): def estimate_cost(self): # SAME name, SAME parameters return self.copies * 2.50 class BookletJob(PrintJob): def __init__(self, ref, copies, pages): super().__init__(ref, copies) self.pages = pages def estimate_cost(self): # SAME name, SAME parameters return self.copies * self.pages * 0.08
// the shared shape, and a fallback price public class PrintJob { protected String ref; protected int copies; public double estimateCost() { return copies * 0.10; } } public class PosterJob extends PrintJob { @Override public double estimateCost() { // SAME name, SAME parameters return copies * 2.50; } } public class BookletJob extends PrintJob { private int pages; @Override public double estimateCost() { // SAME name, SAME parameters return copies * pages * 0.08; } }
Java marks the intention with @Override. It is optional, but it asks the compiler to check that a method with this exact signature really does exist in the parent, which turns a silent typo into a caught error.
Worked example
One call, three different answers
The quoting loop is four lines long and mentions no job type at all.
# the queue holds three different classes, and the loop knows about none of them queue = [PosterJob("KP-118", 6), BookletJob("KP-119", 25, 12), StickerJob("KP-120", 40)] for job in queue: print(job.ref, job.estimate_cost())
Click a job in the queue to see which class’s method actually ran.
Object’s class
Method that runs
Result
Adding a fourth job type next year means writing one new class. The quoting loop, the invoice printer, and every other place that walks the queue keep working untouched, because none of them ever asked what kind of job they were holding.
Explainer
Static polymorphism: overloading
The print works also wants two ways to ask for a quote: the plain one, and one that applies a rush surcharge. Same idea, same name, different information supplied.
In Java, this is real overloading
// three methods, one name, three different parameter lists public class Quote { public double total(int copies) { return copies * 0.10; } public double total(int copies, boolean rush) { return copies * 0.10 * (rush ? 2 : 1); } public double total(int copies, int pages) { return copies * pages * 0.08; } } // the compiler picks the right one from the arguments, before the program runs q.total(40); // 4.00 picks version 1 q.total(40, true); // 8.00 picks version 2 q.total(25, 12); // 24.00 picks version 3
This is where the efficiency comes from: the decision costs nothing at runtime because it was already made during compilation, and callers get one obvious name to remember instead of totalPlain, totalRush and totalByPage.
In Python, it is not
Writing two methods with the same name does not give you two methods. The second definition simply replaces the first, silently, with no error. Python decides what to call by name alone, so a name can only ever refer to one thing at a time.
# this does NOT create two versions class Quote: def total(self, copies): return copies * 0.10 def total(self, copies, rush): # this REPLACES the one above return copies * 0.10 * (2 if rush else 1) q = Quote() q.total(40, True) # 8.00, this works q.total(40) # TypeError: the first version no longer exists
Python reaches the same goal a different way: one method with default parameter values, so the caller may supply the extra information or leave it out.
# one method, several ways to call it: Python's answer to overloading class Quote: def total(self, copies, pages=1, rush=False): cost = copies * pages * 0.08 if pages > 1 else copies * 0.10 return cost * 2 if rush else cost q.total(40) # 4.00 q.total(40, rush=True) # 8.00 q.total(25, 12) # 24.00
Same three call shapes, same three answers, one definition. If a question asks you to apply static polymorphism and you are writing Python, say plainly that Python has no true overloading and show the default-parameter equivalent: that earns the mark, whereas two identical method names does not.
Explainer
Comparing the two forms
Real example: PosterJob.estimate_cost() replacing PrintJob.estimate_cost(), chosen when the loop reaches that object.
Advantage
- New types plug in without touching existing code (the quoting loop never changes, so adding a job type cannot break the jobs that already work)
Disadvantage
- Harder to follow when reading (the line says estimate_cost(), but which of four versions runs is only knowable at runtime, so a debugger is often needed to be sure)
Real example: total(copies) and total(copies, rush) in the same Java class, chosen by the compiler.
Advantage
- No runtime cost and one name to learn (the choice is fixed during compilation, and callers avoid memorising three near-identical method names)
Disadvantage
- Fixed at compile time, and not universal (it cannot adapt to what an object turns out to be, and Python does not offer it at all)
Try it yourself
Watch the right version get chosen
The Kirkstall classes, live and editable. Two experiments worth running: add a fourth job class to the queue without touching the loop, and uncomment the final block to watch Python quietly discard a method when you try to overload it.
Running in offline fallback mode. The full Python engine couldn’t load (blocked school network, or no internet), so this console switched automatically to a small hand-written engine. It runs this page’s scenario, including overriding, dynamic dispatch and Python’s replace-not-overload behaviour, but it isn’t full Python.
Check your understanding
A class defines total(int copies) and total(int copies, boolean rush). A subclass then defines total(int copies) again. Which forms of polymorphism are present?
Common mistake
B3.2.2RecurringOverriding and overloading are swapped constantly. Because the words look alike, candidates describe one and name the other, and the mark is lost on the label rather than on the understanding. The deciding detail is the parameter list: identical means overriding, different means overloading.
A second error appears in Python answers: writing two methods with the same name and different parameters, and calling it overloading. Python accepts the code, so it looks right, but only the second definition survives.
Answer frame that scores: name the mechanism → state whether the parameter lists match → say when the choice is made, at compile time or at runtime.
What a weak answer looks like: “PosterJob overloads estimate_cost() so it can calculate its own price.” The mechanism described is right, the name given to it is wrong, and it is the name being examined.
What it should say: “PosterJob overrides estimate_cost(): it declares the method with the same name and the same parameters as PrintJob, so at runtime the version belonging to the actual object is the one that executes.” Mechanism named, signature stated, timing given.
Revision questions
Past-paper style. Write an answer, then reveal the marking points and mark yourself.
[2]Using the Kirkstall queue, distinguish between method overriding and method overloading.
Marking points
评分要点
- Method overriding: a subclass in the Kirkstall queue redefines a method it inherited, keeping the same name and parameters but giving its own behaviour.
- Method overloading: defining several methods with the same name but different parameter lists, chosen by the arguments supplied.
- Overriding replaces an inherited method; overloading provides same-named variants with different signatures.
- 方法重写:Kirkstall 队列中的一个子类重新定义了一个它继承来的方法,保持相同的名称和参数,但给出自己的行为。
- 方法重载:定义几个同名但形参列表不同的方法,由所提供的实参决定用哪个。
- 重写替换一个继承来的方法;重载提供签名不同的同名变体。
[4]The print works adds laminated menus, priced at 1.20 per copy plus a fixed 6.00 setup charge. Construct the class so the existing quoting loop prices it without modification.
Marking points
评分要点
- class LaminatedMenu inherits from the same base class as the other quoted items.
- It overrides the pricing method (the one the quoting loop calls) with its own formula: 1.20 per copy plus a fixed 6.00 setup charge.
- Because it exposes the same method name and signature, the existing loop calls it polymorphically with no change.
- The constructor takes the copy count (and any shared attributes via the parent constructor).
- class LaminatedMenu 从与其他报价项相同的基类继承。
- 它用自己的公式重写定价方法(报价循环所调用的那个):每份 1.20 加上固定的 6.00 设置费。
- 因为它公开相同的方法名和签名,现有循环通过多态调用它,无需改动。
- 构造函数接收份数(以及通过父类构造函数接收任何共享属性)。
[3]A student writes two Python methods named total() with different parameter lists and states that this demonstrates static polymorphism. Explain why this is incorrect, and describe what Python does instead.
Marking points
评分要点
- Python does not support true overloading: when two methods named total() are defined, the second definition simply replaces the first.
- So only the last total() exists, and calling it with the wrong number of arguments is an error, not a second overload.
- Also, overloading is resolved at compile time in languages that have it, which is compile-time (static) polymorphism, but Python resolves calls at run time.
- Python instead uses default parameter values or a variable-length argument list to let one method handle different argument counts.
- Python 不支持真正的重载:当定义了两个名为 total() 的方法时,第二个定义直接替换第一个。
- 所以只有最后一个 total() 存在,用错误数量的参数调用它是一个错误,而不是第二个重载。
- 此外,在支持重载的语言中重载是在编译时解决的,那是编译时(静态)多态,而 Python 在运行时解决调用。
- Python 转而用默认形参值或可变长度的参数列表,让一个方法处理不同的参数数量。
Flashcards
Active recall for this statement’s key terms.
Keep this statement’s practice with you
Free account: unlock every revision question and flashcard for B3.2.2, and every other IB CS 2027 statement.