CS1302 Introduction to Computer Programming
from manim import *
%reload_ext divewidgetsDefinitions¶
Why object-oriented programming?
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
ScenecalledHelloWorld constructed byplaying an animation thatWrites theTextmessage'Hello, World!'.
Complicated animations can be created without too many lines of code:
Manim
%%manim -ql --progress_bar=none --disable_caching --flush_cache -v ERROR Test
class Test(Scene):
def construct(self):
### BEGIN SOLUTION
self.play(Write(Circle()))
self.play(FadeIn(Square()))
self.play(GrowFromCenter(Circle()))
### END SOLUTION- OOP encapsulates implementation details while
- making programming expressive.
What is an object?
object is a class/type like int, float, str, and bool.
isinstance(object, type)Almost everything in Python is an object, or more precisely, an instance of type object.
(
isinstance(1, object)
and isinstance(1.0, object)
and isinstance("1", object)
and isinstance(True, object)
and isinstance(None, object)
and isinstance(__builtin__, object)
and isinstance(object, object)
)A function is also a object.
isinstance(print, object) and isinstance(range, object)Python treats functions as first-class objects that can be
- passed as arguments to other functions,
- assigned to variables, and
- returned as values.
A simple illustration is as follows.
%%optlite -h 300
def f(f):
return f
f = f(f)A non-trivial illustration is decorator to be explained in a subsequent lecture.
While an object is a type, is a type an object? Check using isinstance.
### BEGIN SOLUTION
isinstance(type, object)
### END SOLUTIONCan an object has multiple types?
An object can be an instance of more than one types.
For instance, True is an instance of bool, int, and object:
isinstance(True, bool) and isinstance(True, int) and isinstance(True, object)issubclass(bool, int) and issubclass(int, object)type(True)returns the immediate type of an object.- The sequences of base classes can be returned by
mro(method resolution order).
print("type of True:", type(True))
print("MRO of True:", type(True).mro())Check whether type is a subclass of object and vice versa. (Is the result reasonable?)
### BEGIN SOLUTION
issubclass(object, type), issubclass(type, object)
### END SOLUTIONWhat is an attribute?
The structure and behavior of an object is governed by its attributes.
To check if an object has a particular attribute:
complex("1+j")hasattr(complex("1+j"), "imag"), hasattr("1+j", "imag")To list all attributes of an object:
dir(complex("1+j"))Different objects of a class have the same set of attributes as that of the class.
dir(complex("1+j")) == dir(complex(1)) == dir(complex)A subclass also inherits the attributes of its base classes.
dir(bool) == dir(int) # subset relation in generalDifferent objects of the same class can still behave differently because their attribute values can be different.
complex("1+j").imag == complex(1).imagAn attribute can also be a function, which is called a method or member function.
complex.conjugate(complex(1, 2)), type(complex.conjugate)A method can be accessed by objects of the class:
complex(1, 2).conjugate(), type(complex(1, 2).conjugate)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.
callable(complex(1, 2).conjugate), complex(1, 2).conjugate.__self__Object Aliasing¶
When are two objects identical?
The keyword is checks whether two objects are the same object:
def f(f):
return f
f(f) is fIs 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 checks 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.
%%optlite -h 400
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?
%%optlite -h 350
print(10**10 is 10**10)
print(10**100 is 10**100)%%optlite -h 350
x = y = "abc"
print(x is y)
print(y is "abc")
print(x + y is x + "abc")Indeed, we normally gets a SyntaxWarning when using is with a literal.
10 is 10, "abc" is "abc"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 obtain 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.
File Objects¶
How to read a text file?
Consider reading a csv (comma separated value) file:
!more 'contact.csv'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 fileopenis a function that creates a file object and assigns it tof.- Associated with the file object:
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())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")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.closedWe can iterate a file object in a for loop,
which implicitly call 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__")Print 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 SOLUTIONHow 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?
!lsTo 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}- 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}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}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)
breakHow to split and join strings?
A string can be split according to a delimiter using the split method.
record.split(",")The list of substrings can be joined back together using the join methods.
print("\n".join(record.split(",")))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 SOLUTIONPrint 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 SOLUTIONOperator Overloading¶
Recall that adding str to int raises a type error. The following code circumvented this by OOP.
%%optlite -l -h 400
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 +?
What is overloading?¶
Recall that the addition operation + behaves differently for different types.
%%optlite -h 300
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.- We can also have function overloading to create generic functions.
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
...%%optlite -h 500
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}")It can get quite messy with 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}")What about new data types?
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}")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 idea is to treat an implementation as a datum that can be returned by the operand types.
x + yis a syntactic sugar that- invokes the method
type(x).__add__(x,y)oftype(x)to do the addition.
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- 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?)
Why does python return a NotImplemented object instead of raising an error/exception?
- This allows
+to continue to handle the addition by - dispatching on
Fractionto call its reverse addition method__radd__.
%%optlite -h 500
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)}")The OOP techniques involved are formally called:
- Polymorphism: Different types can have different implementations of the same method such as
__add__. - Single dispatch: The implementation is chosen based on one single type at a time.
+calls__add__of the first operand, and if not properly implemented for the second operand type,__radd__of the second operand.
- 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 overloads the operator.
Explain how the addition operation for the class MyStr behaves differently as compared to that of str.
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))
MyStr(1) + 2, 2 + MyStr(1)Solution to Exercise 8
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__.