Objects

Why object-oriented programming?

%%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 defines

  • HelloWorld as a Scene

  • constructed by

  • playing an animation that Write

  • the Text message 'Hello, World!'.

Exercise Try changing

  • Mobjects: Text('Hello, World!') to MathTex(r'\int tan(x)\,dx = -\ln(\cos(x))') or Circle() or Square().

  • Animation objects: Write to FadeIn or GrowFromCenter.

See the documentation and tutorial for other choices.

More complicated behavior can be achieved by using different objects.

%%html
<iframe width="800" height="450" src="https://www.youtube.com/embed/ENMyFGmq5OA" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

What is an object?

Almost everything is an object in Python.

isinstance?
isinstance(1, object), isinstance(1.0, object), isinstance("1", object)
(True, True, True)

A function is also a first-class object object.

isinstance(print, object), isinstance("".isdigit, object)
(True, True)

A data type is also an object.

# chicken and egg relationship
isinstance(type, object), isinstance(object, type), isinstance(object, object)
(True, True, True)

Python is a class-based object-oriented programming language:

  • Each object is an instance of a class (also called type in Python).

  • An object is a collection of members/attributes, each of which is an object.

hasattr?
hasattr(str, "isdigit")
True

Different objects of a class

  • have the same set of attributes as that of the class, but

  • the attribute values can be different.

dir?
dir(1) == dir(int), complex(1, 2).imag != complex(1, 1).imag
(True, True)

How to operate on an object?

  • A class can define a function as an attribute for all its instances.

  • Such a function is called a method or member function.

complex.conjugate(complex(1, 2)), type(complex.conjugate)
((1-2j), method_descriptor)

A method can be accessed by objects of the class:

complex(1, 2).conjugate(), type(complex(1, 2).conjugate)
((1-2j), builtin_function_or_method)

complex(1,2).conjugate is a callable object:

  • Its attribute __self__ is assigned to complex(1,2).

  • When called, it passes __self__ as the first argument to complex.conjugate.

callable(complex(1, 2).conjugate), complex(1, 2).conjugate.__self__
(True, (1+2j))

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 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__")
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

Exercise 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 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
contact.csv  media  Objects.ipynb  private  __pycache__

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.

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

Exercise 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

Exercise 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 SOLUTION
CHAN, Tai Ming

Operator Overloading

What is overloading?

Recall that the addition operation + behaves differently for different types.

for x, y in (1, 1), ("1", "1"), (1, "1"):
    print(f"{x!r:^5} + {y!r:^5} = {x+y!r}")
  1   +   1   = 2
 '1'  +  '1'  = '11'
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-31-e005e067083e> in <module>
      1 for x, y in (1, 1), ("1", "1"), (1, "1"):
----> 2     print(f"{x!r:^5} + {y!r:^5} = {x+y!r}")

TypeError: unsupported operand type(s) for +: 'int' and 'str'
  • 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.

How to 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 different implementations together:

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}")
Do integer summation...
    1      +     1      = 2
Do string concatenation...
   '1'     +    '1'     = '11'
Return a TypeError...
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-32-180f8fd48979> in <module>
     10 
     11 for x, y in (1, 1), ("1", "1"), (1, "1"):
---> 12     print(f"{x!r:^10} + {y!r:^10} = {add_case_by_case(x,y)!r}")

<ipython-input-32-180f8fd48979> in add_case_by_case(x, y)
      6     else:
      7         print("Return a TypeError...")
----> 8     return x + y  # replaced by internal implementations
      9 
     10 

TypeError: unsupported operand type(s) for +: 'int' and 'str'

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}")
    1      +    1.1     = 2.1
    1      +   (1+2j)   = (2+2j)
  (1, 2)   +   (1, 2)   = (1, 2, 1, 2)

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}")
1/2 + 1 = 3/2
1 + 1/2 = 3/2

Weaknesses of the naive approach:

  1. New data types require rewriting the addition operation.

  2. A programmer may not know all other types and combinations to rewrite the code properly.

How to have data-directed programming?

The idea is to treat an implementation as a datum that can be returned by the operand types.

  • x + y is a syntactic sugar that

  • invokes the method type(x).__add__(x,y) of type(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
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?)

Why does python return a NotImplemented object instead of raising an error/exception?

  • This allows + to continue to handle the addition by

  • dispatching on Fraction to call its reverse addition method __radd__.

%%mytutor -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 object-oriented programming techniques involved are formally called:

  • Polymorphism: Different types can have different implementations of the __add__ method.

  • Single dispatch: The implementation is chosen based on one single type at a time.

Remarks:

  • 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.

Object Aliasing

When are two objects identical?

The keyword is checks whether two objects are the same object:

sum = 1 .__add__(1.0)
sum is NotImplemented, sum is None
(True, False)

Is is the same as ==?

is is faster.

%%timeit
sum == NotImplemented
40.6 ns ± 0.301 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
%%timeit
sum is NotImplemented
35.4 ns ± 0.235 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
  • is checks whether two objects occupy the same memory but

  • == calls the method __eq__.

1 is 1, 1 is 1.0, 1 == 1.0
<>:1: SyntaxWarning: "is" with a literal. Did you mean "=="?
<>:1: SyntaxWarning: "is" with a literal. Did you mean "=="?
<>:1: SyntaxWarning: "is" with a literal. Did you mean "=="?
<>:1: SyntaxWarning: "is" with a literal. Did you mean "=="?
<ipython-input-40-c2ea26d9f831>:1: SyntaxWarning: "is" with a literal. Did you mean "=="?
  1 is 1, 1 is 1.0, 1 == 1.0
<ipython-input-40-c2ea26d9f831>:1: SyntaxWarning: "is" with a literal. Did you mean "=="?
  1 is 1, 1 is 1.0, 1 == 1.0
(True, False, True)

To see this, we can use the function id which returns an id number for an object based on its memory location.

%%mytutor -h 400
x, y = complex(1, 2), complex(1, 2)
z = x

for expr in ("id(x)", "id(y)", "id(z)", 
             "x == y == z", "x is y", "x is z"):
    print(expr, eval(expr))

As the box-pointer diagram shows:

  • x is not y because they point to objects at different memory locations,
    even though the objects have the same type and value.

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

Can we use is instead of == to compare integers/strings?

10 ** 10 is 10 ** 10, 10 ** 100 is 10 ** 100
<>:1: SyntaxWarning: "is" with a literal. Did you mean "=="?
<>:1: SyntaxWarning: "is" with a literal. Did you mean "=="?
<ipython-input-42-a04e29a6475c>:1: SyntaxWarning: "is" with a literal. Did you mean "=="?
  10 ** 10 is 10 ** 10, 10 ** 100 is 10 ** 100
(True, False)
x = y = "abc"
y = "abc"
x is y, y is "abc", x + y is x + "abc"
<>:3: SyntaxWarning: "is" with a literal. Did you mean "=="?
<>:3: SyntaxWarning: "is" with a literal. Did you mean "=="?
<ipython-input-43-47337f0e97d3>:3: SyntaxWarning: "is" with a literal. Did you mean "=="?
  x is y, y is "abc", x + y is x + "abc"
(True, True, False)

The behavior is not entirely predictable because:

  • it is possible to avoid storing the same integer/string at different locations by interning, but

  • it is impractical to always avoid it.

When should we use is?

is can be used for built-in constants such as None and NotImplemented
because there can only be one instance of each of them.