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 specificScene
that isconstruct
ed byplay
ing an animation thatWrite
the
TextMobject
of the message'Hello, World!'
.
Exercise Try changing
Mobjects:
TextMobject('Hello, World!')
toTexMobject(r'E=mc^2')
orCircle()
orSquare()
.Animation objects:
Write
toFadeIn
orGrowFromCenter
.
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 tocomplex(1,2)
.When called, it passes
__self__
as the first argument tocomplex.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
open
is 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())
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.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'
toopen
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:
New data types require rewriting the addition operation.
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 thatinvokes 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
1/2 + 1 = 3/2
1 + 1/2 = NotImplemented
The first case calls
Fraction.__add__
, which provides a way to addint
toFraction
.The second case calls
int.__add__
, which cannot provide any way of addingFraction
toint
. (Why not?)
Why return a NotImplemented
object instead of raising an error/exception?
This allows
+
to continue to handle the addition bydispatching 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 noty
because they point to objects at different memory locations,
even though the objects have the same type and value.x
isz
because the assignmentz = x
bindsz
to the same memory locationx
points to.
z
is said to be an alias (another name) ofx
.
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 because1
isint
but1.
isfloat
.==
calls the method__eq__
offloat
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.