Abstract¶
Just as closures encapsulate the local environment in functional programming, object-oriented programming (OOP) encapsulates properties and methods within objects, mirroring entities in the physical world. The concepts of inheritance and polymorphism are introduced by demonstrating how to use the methods of file and string objects, and how operators can be overloaded by single dispatch on the types of the operands.
from manim import *
%load_ext divewidgetsOOP¶
Object-oriented programming (OOP) is a programming paradigm based on the concept of objects, which have their own properties and methods that define their behavior.
Motivation¶
Why OOP? Let’s write the Hello-World program with OOP:
%%manim -ql --progress_bar=none --disable_caching --flush_cache -v ERROR HelloWorld
class HelloWorld(Scene):
def construct(self):
self.play(Write(Text("Hello, World!")))The above code creates a video by simply defining
a
ScenecalledHelloWorldconstructed byplaying an animation thatWrites theTextmessage'Hello, World!'.
Complicated animations can be created without too many lines of code because
OOP makes programming expressive by
encapsulating different variables (called properties) and functions (called methods) by the entities (called objects) they apply to and
abstracting away the internal implementation details.
An object is essentially a closure.
Manim
%%manim -ql --progress_bar=none --disable_caching --flush_cache -v ERROR InscribedCircle
class InscribedCircle(Scene):
def construct(self):
### BEGIN SOLUTION
self.play(FadeIn(Square()))
self.play(GrowFromCenter(Circle()))
### END SOLUTIONClasses and Objects¶
Python is a class-based object-oriented programming (OOP) language:
Each object is an instance of a class/type, which can be a subclass of one or more base classes.
An object is a collection of members/attributes, each of which is an object.
What is an object?
object is a class/type like int, float, str, and bool.
isinstance(object, type)TrueRecall that a function is also a first-class object, or more precisely, an instance of type object:
isinstance(print, object) and isinstance(range, object)TrueAlmost everything in Python is an object:
(
isinstance(1, object)
and isinstance(1.0, object)
and isinstance("1", object)
and isinstance(True, object)
and isinstance(None, object)
and isinstance(__builtins__, object)
and isinstance(object, object)
)TrueWhile an object is a type, is a type an object too? Check using isinstance in
the cell below.
### BEGIN SOLUTION
isinstance(type, object)
### END SOLUTIONTrue%%hermes --no-context
Explain in a paragraph the chicken-and-egg relationship between
type and object. Shouldn't we define the class first before
its instance?🌿 Fork/new: main (will create on click)
See Also
In Python, type and object are each other’s parent — object is the base class for everything, but object itself is an instance of type; and type is a subclass of object, yet type is also the class that creates object (and itself — type is its own metaclass). This mutual dependency is the famous “chicken-and-egg” at the root of Python’s object model.
Practically, yes — you must define a class before creating its instances. The circularity only exists at the very top of the hierarchy. For your code, the class always comes first:
class Point:
pass
p = Point() # OK — class existsPython resolves the top-level loop by making type self-referential (type(type) is type), so the circle doesn’t cause a problem — but for everything else, the order is strictly: class definition → instance creation.
An object can be an instance of more than one type. For instance, True is an
instance of bool, int, and object:
isinstance(True, bool) and isinstance(True, int) and isinstance(True, object)Trueissubclass(bool, int) and issubclass(int, object)Truebool.__bases__, int.__bases__((int,), (object,))In particular, True and False have the integer values 1 and 0
respectively.[2]
True == 1, False == 0(True, True)Check whether type is a subclass of object and vice versa using
issubclass.
(Is there a chicken-and-egg problem?)
### BEGIN SOLUTION
issubclass(object, type), issubclass(type, object)
### END SOLUTION(False, True)%%hermes --no-context
Explain in one paragraph why making `object` a subclass of `type` in Python
can lead to conflicts in inheritance.🌿 Fork/new: main (will create on click)
See Also
Making object a subclass of type would create a circular inheritance chain: object → type → object (since type already inherits from object). This breaks Python’s C3 linearization — the algorithm that computes the Method Resolution Order (MRO) — because there’s no acyclic topological ordering for the graph. In practice, every attribute lookup on either object or type would loop infinitely: looking up an attribute on object would delegate to type, which would delegate back to object, and so on. Python prevents this by keeping object as the true root (with no base) and only making type inherit from object, while the metaclass relationship (type is the class of object) runs on a separate axis.
Attributes¶
The structure and behavior of an object is governed by its attributes.
To check if an object has a particular attribute:
complex("1+j")(1+1j)hasattr(complex("1+j"), "imag"), hasattr("1+j", "imag")(True, False)To list all attributes of an object:
print(dir(complex("1+j")))['__abs__', '__add__', '__bool__', '__class__', '__complex__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__mul__', '__ne__', '__neg__', '__new__', '__pos__', '__pow__', '__radd__', '__reduce__', '__reduce_ex__', '__repr__', '__rmul__', '__rpow__', '__rsub__', '__rtruediv__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', 'conjugate', 'imag', 'real']
Different objects of a class share the class attributes:
dir(complex("1+j")) == dir(complex(1)) == dir(complex)TrueDifferent objects of the same class can still behave differently because their attribute values can be different.
complex("1+j").imag, complex(1).imag, complex.imag(1.0, 0.0, <member 'imag' of 'complex' objects>)An attribute can also be a function, which is called a method or member function.
complex.conjugate(complex(1, 2)), type(complex.conjugate)((1-2j), method_descriptor)A method of a class can be accessed by its instances:
complex(1, 2).conjugate(), type(complex(1, 2).conjugate)((1-2j), builtin_function_or_method)callable(complex(1, 2).conjugate), complex(1, 2).conjugate.__self__(True, (1+2j))Why can we call conjugate() without an argument?
conjugate() without an argument?complex(1,2).conjugate is a callable object:
Its attribute
__self__is assigned tocomplex(1,2).When called, it passes
__self__as the first argument tocomplex.conjugate.
A subclass also inherits the attributes of its base classes:
for attribute in dir(bool):
assert attribute in dir(int)For instance, ``boolinherits the methods for arithmetics from the base classint`:
True + 1, False * True(2, 0)True.imag, True.conjugate()(0, 1)A subclass does not necessarily have the same set of attributes as that
defined by its base class. A subclass can inherit attributes from multiple base
classes and define its own attributes. Different implementations of the same
method are resolved using the method resolution order
(mro):
bool.mro()[bool, int, object]%%hermes --no-context
Explain in a paragraph or two how a subclass inherits from multiple
base classes in Python using the C3 linearization.🌿 Fork/new: main (will create on click)
See Also
When a class inherits from multiple bases, Python must decide the Method Resolution Order (MRO) — the linear sequence in which it looks up methods. The C3 linearization algorithm computes this by merging the MROs of each base class while respecting two constraints: (1) the current class appears first, and (2) each base’s own MRO is preserved as a subsequence (monotonicity). Concretely, for class C(A, B), C3 first computes the MROs of A and B, then merges them left-to-right: it picks the first element of the leftmost list whose value doesn’t appear later in any remaining list as a “head blocker.” This ensures that if A lists its parents before B does, those parents take priority — giving left-to-right precedence to the base list while avoiding contradictions. If no consistent merge exists (e.g., a diamond with conflicting orders), C3 raises a TypeError rather than silently picking an ambiguous path.
%%hermes --no-context
Explain in a paragraph why int is a virtual subclass of `numbers.Complex`
but not a subclass of complex?
--
import numbers
assert issubclass(int, numbers.Complex) and not issubclass(int, complex)🌿 Fork/new: main (will create on click)
See Also
numbers.Complex is an abstract base class (ABC) — it defines a mathematical interface, not a concrete implementation. Python’s numbers module uses virtual subclass registration (register()) to declare that int, float, complex, and fractions.Fraction all satisfy the Complex interface, without requiring them to inherit from numbers.Complex. This is a duck-typing pattern: “if it provides real and imag, it behaves like a complex number.” Meanwhile, complex is the concrete built-in type. int shares no inheritance relationship with complex — they are siblings under object. Making int inherit from complex would be semantically wrong (an integer isn’t a complex value with a zero imaginary part in terms of implementation) and would pollute the MRO with unnecessary method resolution through complex’s machinery.
Object Aliasing¶
Now is the time to differentiate values from objects.
When are two objects identical?
The keyword is checks whether two objects are the same object:
%%pytutor
def i(i):
return i
print(i(i) is i)Is is the same as ==?
is is slightly faster because:
issimply checks whether two objects occupy the same memory, but==calls the method (__eq__) of the operands to check the equality in value.
To see this, we can use the function id which returns an id number for an object based on its memory location.
%%pytutor
x = y = complex(1, 0)
z = complex(1, 0)
print(x == y == z == 1.0)
x_id = id(x)
y_id = id(y)
z_id = id(z)
print(x is y) # id(x) == id(y)
print(x is not z) # id(x) != id(z)As the box-pointer diagram shows:
xisybecause the assignmentx = ybindsyto the same memory locationxpoints to.yis said to be an alias (another name) ofx.xis notzbecause they point to objects at different memory locations,
even though the objects have the same type and value.
Can we use is instead of == to compare integers/strings?
%%pytutor
print(10**10 is 10**10)
print(10**100 is 10**100)%%pytutor
x = y = "abc"
print(x is y)
print(y is "abc")
print(x + y is x + "abc")Indeed, we normally get a SyntaxWarning when using is with a literal.
10 is 10, "abc" is "abc"<>:1: SyntaxWarning: "is" with 'int' literal. Did you mean "=="?
<>:1: SyntaxWarning: "is" with 'str' literal. Did you mean "=="?
<>:1: SyntaxWarning: "is" with 'int' literal. Did you mean "=="?
<>:1: SyntaxWarning: "is" with 'str' literal. Did you mean "=="?
/tmp/ipykernel_18322/1875286903.py:1: SyntaxWarning: "is" with 'int' literal. Did you mean "=="?
10 is 10, "abc" is "abc"
/tmp/ipykernel_18322/1875286903.py:1: SyntaxWarning: "is" with 'str' literal. Did you mean "=="?
10 is 10, "abc" is "abc"
(True, True)When using is with a literal, the behavior is not entirely predictable because
Python tries to avoid storing the same value at different locations by interning but
interning is not always possible/practical, especially when the same value is obtained in different ways.
Hence, is should only be used for built-in constants such as None because there can only be one instance of each of them.
Note that the issue is not specific to literals, as shown below. We cannot rely on SyntaxWarning for using is properly.
x, y = 10**100, 10**100
print(x is y)
x, y = 10**10, 10**10
print(x is y)False
True
Using Methods¶
Like a Swiss army knife, an object is a versatile tool that can perform a variety of tasks. By invoking its methods, you can manipulate the object’s data, perform computations, or interact with other objects.
File Objects¶
How to read a text file?
Consider reading a csv (comma separated value) file:
!more 'contact.csv'name, email, phone
Amelia Hawkins,dugorre@lufu.cg,(414) 524-6465
Alta Perez,bos@fiur.sc,(385) 247-9001
Tai Ming Chan,tmchan@cityu.edu.hk,(634) 234-7294
Annie Zimmerman,okodag@saswuf.mn,(259) 862-1082
Eula Crawford,ve@rorohte.mx,(635) 827-9819
Clayton Atkins,vape@nig.eh,(762) 271-7090
Hallie Day,kozzazazi@ozakewje.am,(872) 949-5878
Lida Matthews,joobu@pabnesis.kg,(213) 486-8330
Amelia Pittman,nulif@uposzag.au,(800) 303-3234
To read the file by a Python program:
f = open("contact.csv") # create a file object for reading
print(f.read()) # return the entire content
f.close() # close the filename, email, phone
Amelia Hawkins,dugorre@lufu.cg,(414) 524-6465
Alta Perez,bos@fiur.sc,(385) 247-9001
Tai Ming Chan,tmchan@cityu.edu.hk,(634) 234-7294
Annie Zimmerman,okodag@saswuf.mn,(259) 862-1082
Eula Crawford,ve@rorohte.mx,(635) 827-9819
Clayton Atkins,vape@nig.eh,(762) 271-7090
Hallie Day,kozzazazi@ozakewje.am,(872) 949-5878
Lida Matthews,joobu@pabnesis.kg,(213) 486-8330
Amelia Pittman,nulif@uposzag.au,(800) 303-3234
Why close a file?
If not, depending on the operating system,
other programs may not be able to access the file, and
changes may not be written to the file.
To ensure a file is closed properly, we can use the with statement:
with open("contact.csv") as f:
print(f.read())name, email, phone
Amelia Hawkins,dugorre@lufu.cg,(414) 524-6465
Alta Perez,bos@fiur.sc,(385) 247-9001
Tai Ming Chan,tmchan@cityu.edu.hk,(634) 234-7294
Annie Zimmerman,okodag@saswuf.mn,(259) 862-1082
Eula Crawford,ve@rorohte.mx,(635) 827-9819
Clayton Atkins,vape@nig.eh,(762) 271-7090
Hallie Day,kozzazazi@ozakewje.am,(872) 949-5878
Lida Matthews,joobu@pabnesis.kg,(213) 486-8330
Amelia Pittman,nulif@uposzag.au,(800) 303-3234
The with statement applies to any context manager that provides the methods
__enter__for initialization, and__exit__for finalization.
with open("contact.csv") as f:
print(f, hasattr(f, "__enter__"), hasattr(f, "__exit__"), sep="\n")<_io.TextIOWrapper name='contact.csv' mode='r' encoding='UTF-8'>
True
True
f.__enter__is called after the file object is successfully created and assigned tof, andf.__exit__is called at the end, which closes the file.f.closedindicates whether the file is closed.
f.closedTrueWe can iterate a file object in a for loop,
which implicitly calls the method __iter__ to read a file line by line.
with open("contact.csv") as f:
for line in f:
print(line, end="")
hasattr(f, "__iter__")name, email, phone
Amelia Hawkins,dugorre@lufu.cg,(414) 524-6465
Alta Perez,bos@fiur.sc,(385) 247-9001
Tai Ming Chan,tmchan@cityu.edu.hk,(634) 234-7294
Annie Zimmerman,okodag@saswuf.mn,(259) 862-1082
Eula Crawford,ve@rorohte.mx,(635) 827-9819
Clayton Atkins,vape@nig.eh,(762) 271-7090
Hallie Day,kozzazazi@ozakewje.am,(872) 949-5878
Lida Matthews,joobu@pabnesis.kg,(213) 486-8330
Amelia Pittman,nulif@uposzag.au,(800) 303-3234TruePrint only the first 5 lines of the file contact.csv.
with open("contact.csv") as f:
### BEGIN SOLUTION
for i, line in enumerate(f):
print(line, end="")
if i >= 5:
break
### END SOLUTIONname, email, phone
Amelia Hawkins,dugorre@lufu.cg,(414) 524-6465
Alta Perez,bos@fiur.sc,(385) 247-9001
Tai Ming Chan,tmchan@cityu.edu.hk,(634) 234-7294
Annie Zimmerman,okodag@saswuf.mn,(259) 862-1082
Eula Crawford,ve@rorohte.mx,(635) 827-9819
How to write to a text file?
Consider backing up contact.csv to a new file:
destination = "private/new_contact.csv"The directory has to be created first if it does not exist:
import os
os.makedirs(os.path.dirname(destination), exist_ok=True)os.makedirs?
!lsObjects.ipynb Objects_.ipynb abc.md media
Objects.md Objects_.md contact.csv private
To write to the destination file:
with open("contact.csv") as source_file:
with open(destination, "w") as destination_file:
destination_file.write(source_file.read())destination_file.write?
!more {destination}name, email, phone
Amelia Hawkins,dugorre@lufu.cg,(414) 524-6465
Alta Perez,bos@fiur.sc,(385) 247-9001
Tai Ming Chan,tmchan@cityu.edu.hk,(634) 234-7294
Annie Zimmerman,okodag@saswuf.mn,(259) 862-1082
Eula Crawford,ve@rorohte.mx,(635) 827-9819
Clayton Atkins,vape@nig.eh,(762) 271-7090
Hallie Day,kozzazazi@ozakewje.am,(872) 949-5878
Lida Matthews,joobu@pabnesis.kg,(213) 486-8330
Amelia Pittman,nulif@uposzag.au,(800) 303-3234
The argument
'w'foropensets the file object to write mode.The method
writewrites the input strings to the file.
We can also use a mode to append new content to a file.
Complete the following code to append new_data to the file destination.
new_data = "Effie, Douglas,galnec@naowdu.tc, (888) 311-9512"
with open(destination, "a") as f:
### BEGIN SOLUTION
f.write("\n")
f.write(new_data)
### END SOLUTION
!more {destination}name, email, phone
Amelia Hawkins,dugorre@lufu.cg,(414) 524-6465
Alta Perez,bos@fiur.sc,(385) 247-9001
Tai Ming Chan,tmchan@cityu.edu.hk,(634) 234-7294
Annie Zimmerman,okodag@saswuf.mn,(259) 862-1082
Eula Crawford,ve@rorohte.mx,(635) 827-9819
Clayton Atkins,vape@nig.eh,(762) 271-7090
Hallie Day,kozzazazi@ozakewje.am,(872) 949-5878
Lida Matthews,joobu@pabnesis.kg,(213) 486-8330
Amelia Pittman,nulif@uposzag.au,(800) 303-3234
Effie, Douglas,galnec@naowdu.tc, (888) 311-9512
How to delete a file?
Note that the file object does not provide any method to delete the file.
Instead, we should use the function remove of the os module.
if os.path.exists(destination):
os.remove(destination)
!ls {destination}ls: cannot access 'private/new_contact.csv': No such file or directory
String Objects¶
How to search for a substring in a string?
A string object has the method find to search for a substring.
E.g., to find the contact information of Tai Ming:
str.find?
with open("contact.csv") as f:
for line in f:
if line.find("Tai Ming") != -1:
record = line
print(record)
breakTai Ming Chan,tmchan@cityu.edu.hk,(634) 234-7294
How to split and join strings?
A string can be split according to a delimiter using the split method.
record.split(",")['Tai Ming Chan', 'tmchan@cityu.edu.hk', '(634) 234-7294\n']The list of substrings can be joined back together using the join methods.
print("\n".join(record.split(",")))Tai Ming Chan
tmchan@cityu.edu.hk
(634) 234-7294
Print only the phone number (last item) in record. Use the method rstrip or strip to remove unnecessary white spaces at the end.
str.rstrip?
### BEGIN SOLUTION
print(record.split(",")[-1].rstrip())
### END SOLUTION(634) 234-7294
Print only the name (first item) in record but with
surname printed first with all letters in upper case
followed by a comma, a space, and
the first name as it is in
record.
E.g., Tai Ming Chan should be printed as CHAN, Tai Ming.
Hint: Use the methods upper and rsplit (with the parameter maxsplit=1).
str.rsplit?
### BEGIN SOLUTION
first, last = record.split(",")[0].rsplit(" ", maxsplit=1)
print("{}, {}".format(last.upper(), first))
### END SOLUTIONCHAN, Tai Ming
Operator Overloading¶
Recall that adding str to int raises a type error. The following code circumvented this by creating a subclass of str that overrides its methods __add__ and __radd__ for addition:
%%pytutor
class MyStr(str):
def __add__(self, a):
return MyStr(str.__add__(self, str(a)))
def __radd__(self, a):
return MyStr(str.__add__(str(a), self))
print(MyStr(1) + 2, 2 + MyStr(1))How does the above code re-implements +?
Recall that the addition operation + behaves differently for different types.
%%pytutor
for x, y in (1, 1), ("1", "1"), (1, "1"):
print(f"{x!r:^5} + {y!r:^5} = {x+y!r}")Having an operator perform differently based on its argument types is called operator overloading.
+is called a generic operator.
Ask an AI agent to design a Rectangle class with
__init__, __str__, and area() method. Evaluate
whether it includes proper error handling for negative
dimensions.
Solution to Exercise 8 #
A well-designed Rectangle class:
class Rectangle:
def __init__(self, width, height):
if width <= 0 or height <= 0:
raise ValueError("Dimensions must be positive.")
self.width = width
self.height = height
def area(self):
return self.width * self.height
def __str__(self):
return f"Rectangle({self.width} x {self.height})"Evaluation points:
__init__validates input (rejects negative or zero dimensions).__str__returns a human-readable string.area()computes and returns the area.Error handling uses
ValueError(notExceptionor bareexcept).
Dispatch on type¶
The strategy of checking the type for the appropriate implementation is called dispatching on type.
A naive idea is to put all the different implementations together:
def add_case_by_case(x, y):
if isinstance(x, int) and isinstance(y, int):
# integer summation
...
elif isinstance(x, str) and isinstance(y, str):
# string concatenation...
...
else:
# Return a TypeError
...%%pytutor
def add_case_by_case(x, y):
if isinstance(x, int) and isinstance(y, int):
print("Do integer summation...")
elif isinstance(x, str) and isinstance(y, str):
print("Do string concatenation...")
else:
print("Return a TypeError...")
return x + y # replaced by internal implementations
for x, y in (1, 1), ("1", "1"), (1, "1"):
print(f"{x!r:^10} + {y!r:^10} = {add_case_by_case(x,y)!r}")One issue is that supporting a new data type would require the addition to be re-implemented.
from fractions import Fraction # non-built-in type for fractions
for x, y in ((Fraction(1, 2), 1), (1, Fraction(1, 2))):
print(f"{x} + {y} = {x+y}")1/2 + 1 = 3/2
1 + 1/2 = 3/2
Another issue is that developers need to fight for a good spot in the conditional statement to implement additions for the new data type.
These issues can be alleviated in some programming languages such as C and Java that support function overloading.
%%hermes --no-context
Explain in a paragraph what function overloading is, and how it can avoid
writing conditional statement to implement a function for different types.🌿 Fork/new: main (will create on click)
See Also
Function overloading lets you define multiple versions of the same-named function that differ in their parameter types or count — the compiler (or dispatcher) picks the right one automatically based on the call site’s argument types. Without overloading, you’d need a single function full of if isinstance(x, int) / elif isinstance(x, str) conditionals to branch on type, making the code harder to read and maintain. With overloading (or @singledispatch in Python), each type gets its own clean implementation, so the caller writes add(a, b) regardless of whether those are ints, strings, or lists — the right version runs with no manual type-checking boilerplate.
However, Python does not support function overloading by default, and even if it did, it would still be quite tedious to implement additions for all possible types and combinations:
for x, y in ((1, 1.1), (1, complex(1, 2)), ((1, 2), (1, 2))):
print(f"{x!r:^10} + {y!r:^10} = {x+y!r}") 1 + 1.1 = 2.1
1 + (1+2j) = (2+2j)
(1, 2) + (1, 2) = (1, 2, 1, 2)
Weaknesses of the naive approach:
New data types require rewriting the addition operation.
A programmer may not know all other types and combinations to rewrite the code properly.
Data-directed programming¶
The solution idea is to treat an implementation as a datum that can be returned by the operand types.
for x, y in (Fraction(1, 2), 1), (1, Fraction(1, 2)):
print(f"{x} + {y} = {type(x).__add__(x,y)}") # instead of x + y1/2 + 1 = 3/2
1 + 1/2 = NotImplemented
The first case calls
Fraction.__add__, which provides a way to addinttoFraction.The second case calls
int.__add__, which cannot provide any way of addingFractiontoint. (Why not?)
NotImplemented object instead of raising an error/exception?This allows
+to continue to handle the addition bydispatching on
Fractionto call its reverse addition method__radd__.
%%pytutor
from fractions import Fraction
def add(x, y):
"""Simulate the + operator."""
sum = x.__add__(y)
if sum is NotImplemented:
sum = y.__radd__(x)
return sum
for x, y in (Fraction(1, 2), 1), (1, Fraction(1, 2)):
print(f"{x} + {y} = {add(x,y)}")x + y is a syntactic sugarx + y invokes the method of the first operand x to do the addition:
x.__add__(y)If NotImplemented object is returned, it invokes the method __radd__ of the second operand y to do the reverse addition:
y.__radd__(x)A method with starting and trailing double underscores in its name is called a dunder method.
Dunder methods are not intended to be called directly. E.g., we normally use
+instead of__add__.Other operators have their corresponding dunder methods that overload the operator.
Explain how the addition operation for the class MyStr behaves differently compared to that of the str class:
class MyStr(str):
def __add__(self, a):
return MyStr(str.__add__(self, str(a)))
def __radd__(self, a):
return MyStr(str.__add__(str(a), self))Solution to Exercise 9
Unlike str which cannot be added to instances of other types such as int, MyStr can be added (concatenated) or reverse added to instances of other types. This is achieved by overloading the + operation with the new implementations of the forward/reverse addition methods __add__ and __radd__.
The OOP techniques involved are formally called:
Polymorphism: Different types can have different implementations of the same method such as
__add__and__radd__.Single dispatch: The implementation such as
+operation is chosen based on one single type at a time.
Ask an AI agent to create a Dog and Cat subclass from an
Animal base class with a speak() method. Verify that
polymorphism works by iterating over a mixed list.
Solution to Exercise 10 #
A polymorphic animal hierarchy:
class Animal:
def speak(self):
return "..."
class Dog(Animal):
def speak(self):
return "Woof!"
class Cat(Animal):
def speak(self):
return "Meow!"
animals = [Dog(), Cat(), Animal()]
for animal in animals:
print(animal.speak())Evaluation points:
DogandCatinherit fromAnimaland overridespeak().Polymorphism works: calling
speak()on anyAnimalinstance dispatches to the correct subclass method.The AI may also mention
super().__init__()for calling parent constructors, or usingisinstance()for type checking.Using
@abstractmethodfromabccould enforce that subclasses implementspeak().
%%hermes --no-context
Explain briefly in a table the differences between polymorphism, inheritance,
overloading, overriding, and type dispatch, in providing different behaviors
of an object.🌿 Fork/new: main (will create on click)
See Also
| Mechanism | How it selects behavior | Python support |
|---|---|---|
| Polymorphism | Same method name on different types → different behavior (duck typing) | Native — just call the method |
| Inheritance | Child reuses parent’s behavior by default (is-a relationship) | Native — class Child(Parent) |
| Overloading | Same name, different parameter signatures | Not natively supported — use defaults, *args, or @singledispatchmethod |
| Overriding | Subclass replaces a parent method with its own version | Native — just redefine the method |
| Type dispatch | Behavior selected by the runtime type of an argument | Via functools.singledispatch |
%%hermes --no-context
Explain in a paragraph or two how operators are overloaded differently in C++
than in Python, i.e., without using single dispatch.🌿 Fork/new: main (will create on click)
See Also
In C++, operator overloading is compile-time resolution based on all operand types — the compiler sees a + b, knows the static types of both a and b, and picks the matching overload from a set of candidate functions. A class can define operator+(const Point&, const Point&) as a free function or Point::operator+(const Point&) as a member, and the compiler does name mangling + overload resolution to select the right one. This is effectively multiple dispatch at compile time — both arguments participate in the selection. Python, by contrast, dispatches at runtime on the left operand’s type via __add__. If a.__add__(b) returns NotImplemented, it falls back to b.__radd__(a), which is a clumsy two-step fallback rather than true multi-argument dispatch. C++ doesn’t need singledispatch or reverse methods because the compiler already knows everything it needs to resolve the call statically.
PyBool_Type(bool) specifiesPyLong_Type(int) as thetp_base(base class) in the source codeSee the source code defining
Falseand the source code definingTrue.