8. Objects

8.1. Object-Oriented Programming

Why object-oriented programming?

import jupyter_manim
from manimlib.imports import *
%%manim HelloWorld -l
class HelloWorld(Scene):
    def construct(self):
        self.play(Write(TextMobject('Hello, World!')))
  • HelloWorld is a specific Scene that is

  • constructed by playing an animation that Write

  • the TextMobject of the message 'Hello, World!'.

Exercise Try changing

  • Mobjects: TextMobject('Hello, World!') to TexMobject(r'E=mc^2') or Circle() or Square().

  • Animation objects: Write to FadeIn or GrowFromCenter.

See the documentation for other choices.

More complicated behavior can be achieved by using different objects.

%%html
<iframe width="912" height="513" 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))

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

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' to 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)

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

8.4. Operator Overloading

8.4.1. 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-32-297be71279db> 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.

8.4.2. 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 with case-by-case checks of operand types.

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-33-85c671bf17e3> 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-33-85c671bf17e3> 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.

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

8.5. Object Aliasing

When are two objects identical?

  • Two objects are the same if they occupy the same memory.

  • The keyword is checks whether two objects are the same object.

  • The function id returns a unique id number for each object.

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

Should we use is or ==?

is is faster but:

1 is 1, 1 is 1., 1 == 1.
(True, False, True)
  • 1 is 1. returns false because 1 is int but 1. is float.

  • == calls the method __eq__ of float which returns mathematical equivalence.

Can we use is for integer comparison?

x, y = 1234, 1234
1234 is 1234, x is y
(True, False)

No. The behavior of is is not entirely predictable.

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.