Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

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 divewidgets

OOP

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 Scene called HelloWorld

  • constructed by playing an animation that

  • Writes the Text message '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 -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 SOLUTION

Classes and Objects

What is an object?

object is a class/type like int, float, str, and bool.

isinstance(object, type)
True

Recall that a function is also a first-class object, or more precisely, an instance of type object:

isinstance(print, object) and isinstance(range, object)
True

Almost 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)
)
True
### BEGIN SOLUTION
isinstance(type, object)
### END SOLUTION
True
%%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?
Loading...
🌿 Fork/new: main (will create on click)

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)
True

As proposed in PEP 285,[1]

bool→subclassint→subclassobject\begin{CD} \text{bool} @>{\text{subclass}}>> \text{int} @>{\text{subclass}}>> \text{object} \end{CD}
  • bool is a subclass of int, and

  • int is a base class of bool.

issubclass(bool, int) and issubclass(int, object)
True
bool.__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)
### 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.
Loading...
🌿 Fork/new: main (will create on click)

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)
True

Different 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))

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.
Loading...
🌿 Fork/new: main (will create on click)
%%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)
Loading...
🌿 Fork/new: main (will create on click)

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)
Loading...

Is is the same as ==?

is is slightly faster because:

  • is simply 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)
Loading...

As the box-pointer diagram shows:

  • x is y because the assignment x = y binds y to the same memory location x points to.
    y is said to be an alias (another name) of x.

  • x is not z because 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)
Loading...
%%pytutor
x = y = "abc"
print(x is y)
print(y is "abc")
print(x + y is x + "abc")
Loading...

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)

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 file
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
  1. open is a function that creates a file object and assigns it to f.

  2. Associated with the file object:

    • read returns the entire content of the file as a string.

    • close flushes and closes the file.

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 to f, and

  • f.__exit__ is called at the end, which closes the file.

  • f.closed indicates whether the file is closed.

f.closed
True

We 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-3234
True
with open("contact.csv") as f:
    ### BEGIN SOLUTION
    for i, line in enumerate(f):
        print(line, end="")
        if i >= 5:
            break
    ### END SOLUTION
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

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?
!ls
Objects.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' for open sets the file object to write mode.

  • The method write writes the input strings to the file.

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)
            break
Tai 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

str.rstrip?
### BEGIN SOLUTION
print(record.split(",")[-1].rstrip())
### END SOLUTION
(634) 234-7294
str.rsplit?
### BEGIN SOLUTION
first, last = record.split(",")[0].rsplit(" ", maxsplit=1)
print("{}, {}".format(last.upper(), first))
### END SOLUTION
CHAN, 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))
Loading...

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}")
Loading...
  • Having an operator perform differently based on its argument types is called operator overloading.

  • + is called a generic operator.

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 (not Exception or bare except).

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}")
Loading...

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.
Loading...
🌿 Fork/new: main (will create on click)

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)

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 + y
1/2 + 1 = 3/2
1 + 1/2 = NotImplemented
  • The first case calls Fraction.__add__, which provides a way to add int to Fraction.

  • The second case calls int.__add__, which cannot provide any way of adding Fraction to int. (Why not?)

%%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)}")
Loading...
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.

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:

  • Dog and Cat inherit from Animal and override speak().

  • Polymorphism works: calling speak() on any Animal instance dispatches to the correct subclass method.

  • The AI may also mention super().__init__() for calling parent constructors, or using isinstance() for type checking.

  • Using @abstractmethod from abc could enforce that subclasses implement speak().

%%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.
Loading...
🌿 Fork/new: main (will create on click)
%%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.
Loading...
🌿 Fork/new: main (will create on click)
Footnotes
  1. PyBool_Type (bool) specifies PyLong_Type (int) as the tp_base (base class) in the source code