08. Introduction to Object-Oriented Programming I#

Last time#

  • Modules

  • Packages

  • Exception

  • Assertion

  • Argument parser

Today#

What is object-oriented programming?#

  • Every object has a data type that defines the kinds of things that programs can do with that object.

    • They have an internal representation through data attributes.

    • They have an interface for interacting with object through methods.


  • For example, consider a list L = ['a', 1, 'b', 2].

    • How is this list represented internally?



    • How to manipulate this list?

    L[1]
    len(L)
    min(L)
    L.pop(1)
    L.append('34567')
    

Class#

  • Create and use your own object with class

  • Create a class object involves…

    1. Define the name

    2. Define the attributes of this class object
      Example: str, tuple, list, dict, …

  • Use a class object involves…

    1. Create new instance for this class object

    2. Define the operations or methods for this class object
      Example: len(), (95,) + (2, 7), L.append(9487), D.keys(), …


Syntax#

class class_name(inheritance_object*):
    """Docstrings of this class object"""
    
    def __init__(self, optional_parameters):
        """Docstrings of this"""
        <your code>
    
    def method1(self, optional_parameters):
        """Docstrings of this method"""
        <your code>
  • We are going to discuss the inheritance in the next lecture.

import math

class Point(object):
    """
    Define a point in two dimensional space
    """
    # Constructor
    def __init__(self, x, y):
        """
        Create instances by the constructor "__init__" and use "self" to refer to that instance
        """
        self.x = x
        self.y = y
    
    def __str__(self):
        """
        Define how does Python react whenever a "Point object" encounters an operand "print"
        """
        return "({}, {})".format(self.x, self.y)
    
    # Define what kind of operations you can do to a "Point object"
    def cartesian(self): 
        """Method 1"""
        return (self.x, self.y)
    
    # Define what kind of operations you can do to a "Point object"
    def polar(self):
        """Method 2"""
        radius = math.sqrt(self.x**2 + self.y**2)
        phi = math.atan2(self.y, self.x)
        return (radius, phi)
    
    # Define what kind of operations you can do to a "Point object"
    def distance(self, other):
        """Method 3"""
        assert type(other) == Point, "All input should be a Point object."
        x_diff_sq = (self.x - other.x)**2
        y_diff_sq = (self.y - other.y)**2
        return math.sqrt(x_diff_sq + y_diff_sq)
    
p1 = Point(3, 4)
p2 = Point(-3, -4)
origin = Point(0, 0)

print(p1)
print(origin)
print(p1.polar())
print(p1.distance(origin))
print(p1.distance(p2))
(3, 4)
(0, 0)
(5.0, 0.9272952180016122)
5.0
10.0

  • What will you get if you try these?

p3 = Point()
Point(1,0).polar()


Exercise 8.1#

Please create an object called Line2D that aims to represent a 2D line.

  1. It has 3 default parameters \((x_0, y_0, m)\).

\[\begin{split} \begin{aligned} (x_0, y_0): & \text{ the coordinate of an arbitrary point on the line} \\ m: & \text{ the slope of the line} \end{aligned} \end{split}\]
  1. It should return a message with its equation when you print it.

Here is your sample code:

class Line2D:
    def __init__(self, x0, y0, m):
        ???

    
    def __str__(self):
        ???

For example, you might get this message when you run the script:

line1 = Line2D(5, 3, 3)

print(line1)

A 2D line: 3x + 1y - 12


Attributes#

  • The data and methods that belong to the class

  • Instance attributes

    • You can think of what kind of data will it looks like in this class Ex: a Point should have a Cartesian coordinate (x, y)

  • Method attributes

    • You can think of what kind of functions can be work in this class only. The methods will define how Python interacts with the object.
      Ex: We can transform a Point from Cartesian coordinate to polar coordinate via method polar() we defined in Point.

  • Class attributes

    • You can regard it as the common attributes of all object that belongs to this class.


class Person:
    # Class attribute
    num_head = 1

    def __init__(self, name, height=None, weight=None, gender=None):
        self.name = name           # Instance attribute
        self.set_gender(gender)
        self.set_height(height)
        self.set_weight(weight)
    
    def set_gender(self, value):   # Method attribute
        """Method: set_gender"""
        if value == "M":
            self.gender = "Male"
        elif value == "F":
            self.gender = "Female"
        else:
            self.gender = "Secret"
    
    def get_gender(self):          # Method attribute
        """Method: get_gender"""
        return self.gender

    def set_height(self, value):   # Method attribute
        """Method: set_height"""
        if value is not None:
            if value <= 0:
                raise ValueError("Height cannot be less than 0.")
            else:
                self.height = value
    
    def get_height(self):          # Method attribute
        """Method: get_height"""
        return self.height
    
    def set_weight(self, value):   # Method attribute
        """Method: set_weight"""
        if value is not None:
            if value <= 0:
                raise ValueError("Weight cannot be less than 0.")
            else:
                self.weight = value
    
    def get_weight(self):          # Method attribute
        """Method: get_weight"""
        return self.weight
    
A = Person("Abigail", gender="Female", height=170, weight=60)
B = Person("Bruce", gender="Male", height=181, weight=80)

# Instance attributes
print("Height of Abigail =", A.height)
print("Height of Bruce =", B.height)
print("Height of Abigail =", A.get_height())
print("Height of Bruce =", B.get_height())

# Class attributes
print("# of Abigail's head =", A.num_head)
print("# of Bruce's head =", B.num_head)

# Update
print("="*50)
A.set_height(175)
B.height = 185
print("Height of Abigail =", A.get_height())
print("Height of Bruce =", B.get_height())
Person.num_head = 2
print("# of Abigail's head =", A.num_head)
print("# of Bruce's head =", B.num_head)
Height of Abigail = 170
Height of Bruce = 181
Height of Abigail = 170
Height of Bruce = 181
# of Abigail's head = 1
# of Bruce's head = 1
==================================================
Height of Abigail = 175
Height of Bruce = 185
# of Abigail's head = 2
# of Bruce's head = 2

Some common special methods#

Method

Meaning

__add__

Define how does Python react whenever this object encounters +

__sub__

Define how does Python react whenever this object encounters -

__eq__

Define how does Python react whenever this object encounters ==

__lt__

Define how does Python react whenever this object encounters <

__gt__

Define how does Python react whenever this object encounters >

__str__

Define how does Python react whenever this object encounters print()

__len__

Define how does Python react whenever this object encounters len()

__iter__

Define how does Python react whenever this object encounters iter()

__next__

Define how does Python get a value while iteration

__getitem__

Define how does Python get a value while iteration

  • For more special methods, please use Google or ChatGPT.


Exercise 8.2#

  • Please add a method attribute check of the object Line2D.

    1. It should have ability to check whether a Point is on the line or not.

    2. When a Point is not on the line, it should calculate the distance between the point and the line and return a message.

class Line2D:
    def __init__(self, x0, y0, slope):
        ???

    
    def __str__(self):
        ???
    
    def check(self, other):
        """Check whether the point is on the line or not"""
        ???
  • For example, you might get this message when you run the script:

line1 = Line2D(5, 3, 3)
p1 = Point(6, 6)
p2 = Point(8, 7)


print(line1)
print(line1.check(p1))
print(line1.check(p2))

A 2D line: 3x + 1y - 12
The point (6, 6) is on the line.
The point (8, 7) is not on the line.
Distance: 1.5811


Iterable object and iterator#

  • Iterable object:

    • An object that can be iterating is called an iterable object.

    • For simplicity, an object that contains __iter__ or __getitem__ is an iterable object.

  • Iterator:

    • An object that follows the Python Iterator Protocol is called an iterator.

    • For simplicity, an object that contains __iter__ and __next__ is an iterator.


How to check?#

  • Use dir() or hasattr()

x = [9,4,8,7]
y = {"0": 87, "5": "abc", 3: "100%"}

# Check attributes by dir()
print(dir(x))
print(dir(y))

# Check attributes by hasattr()
checklist = ["__iter__", "__getitem__", "__next__"]
for item in checklist:
    print("Now check: {}".format(item))
    print("Does x have {}?".format(item), hasattr(x, item))
    print("Does y have {}?".format(item), hasattr(y, item))
['__add__', '__class__', '__class_getitem__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getstate__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']
['__class__', '__class_getitem__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__ior__', '__iter__', '__le__', '__len__', '__lt__', '__ne__', '__new__', '__or__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__ror__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'clear', 'copy', 'fromkeys', 'get', 'items', 'keys', 'pop', 'popitem', 'setdefault', 'update', 'values']
Now check: __iter__
Does x have __iter__? True
Does y have __iter__? True
Now check: __getitem__
Does x have __getitem__? True
Does y have __getitem__? True
Now check: __next__
Does x have __next__? False
Does y have __next__? False
test = iter(x)
print(test)
print(test.__next__())
print(test.__next__())
print("="*50)

for i in iter(x):
    print(i)
print("="*50)

for i in test:
    print(i)
<list_iterator object at 0x000002610248E0E0>
9
4
==================================================
9
4
8
7
==================================================
8
7

Every time you call a for loop#




Create an iterator#

  1. Use __iter__ and __next__

class iterator1:
    def __init__(self, max_num):
        self.max_num = max_num
        self.index = 0

    def __iter__(self):
        return self
        
    def __next__(self):
        self.index += 1
        if self.index < self.max_num:
            return self.index
        else:
            raise StopIteration
testIterator1 = iterator1(5)
for item in testIterator1:
    print(item)

print("End of testIterator1, the first time")
print("="*50)

testIterator2 = iterator1(3)
for item in testIterator2:
    print(item)

print("End of testIterator2")
print("="*50)

for item in testIterator1:
    print(item)

print("End of testIterator1, the second time")
1
2
3
4
End of testIterator1, the first time
==================================================
1
2
End of testIterator2
==================================================
End of testIterator1, the second time
  1. Use __iter__ and generator (yeild)

class iterator2:
    def __init__(self, max_num):
        self.max_num = max_num

    def __iter__(self):
        num = 0             # refresh the initial value every time you invoke the iteration
        while num <= self.max_num:
            yield num       # output this value every loop
            # yield str(num)+"!@#"
            num += 1
testIterator3 = iterator2(4)
for item in testIterator3:
    print(item)

print("End of testIterator3, the first time")
print("="*50)

testIterator4 = iterator2(2)
for item in testIterator4:
    print(item)

print("End of testIterator4")
print("="*50)

for item in testIterator3:
    print(item)

print("End of testIterator3, the second time")
0
1
2
3
4
End of testIterator3, the first time
==================================================
0
1
2
End of testIterator4
==================================================
0
1
2
3
4
End of testIterator3, the second time
  1. Use __getitem__

class iterator3:
    def __init__(self, max_num):
        self.max_num = max_num

    # Python will use __getitem__ to get the value while iteration
    def __getitem__(self, key):
        if key <= self.max_num:
            return key
        else:
            # raise IndexError      # both can work
            raise StopIteration   # both can work
testIterator5 = iterator3(4)
for item in testIterator5:
    print(item)

print("End of testIterator5, the first time")
print("="*50)

testIterator6 = iterator3(2)
for item in testIterator4:
    print(item)

print("End of testIterator6")
print("="*50)

for item in testIterator5:
    print(item)

print("End of testIterator5, the second time")
0
1
2
3
4
End of testIterator5, the first time
==================================================
0
1
2
End of testIterator6
==================================================
0
1
2
3
4
End of testIterator5, the second time

Summary#

  1. Use __iter__ and __next__

    • If there is a method attribute __iter__, Python will use iter() to create an iterator.

    • If there is a method attribute __next__, Python will use iter().__next__ to get value while iteration.

    • The index of the iteration is an instance attribute.

    • Need to invoke the iterator object if you want to use this iterator.


  1. Use __iter__ and generator (yeild)

    • If there is a method attribute __iter__, Python will use iter() to create an iterator.

    • Although there is no method attribute __next__, Python can still get value via yeild in the method attribute __iter__.

    • Because the index of the iteration is not an instance attribute, the initial value will be refreshed everytime Python invokes iter().


  1. Use __getitem__

    • __getitem__ requires a positional argument key.

    • Everytime you call a loop, Python will create a number from 0 to \(\infty\) as the key of __getitem__ sequentially.

    • Terminate the iteration when it meets the stop condition.

Don't copy this
class Line2D:
    def __init__(self, x0, y0, slope) -> None:
        if slope < 0:
            self.coeff = (-slope, 1, slope*x0 -y0)
        else: 
            self.coeff = (slope, -1, y0 - slope*x0)
        self.eq = lambda x, y: self.coeff[0]*x + self.coeff[1]*y + self.coeff[2]
        # self.eq = lambda x, y: self.m * (x - self.x0) + self.y0 - y
    
    def __str__(self):
        string = "A 2D line: {}x".format(self.coeff[0])
        if self.coeff[1] < 0:
            string += " + {}y".format(abs(self.coeff[1]))
        else:
            string += " - {}y".format(abs(self.coeff[1]))
        if self.coeff[2] < 0:
            string += " + {}".format(abs(self.coeff[2]))
        else:
            string += " - {} = 0".format(self.coeff[2])
        return string
    
    def check(self, other):
        """Check whether the point is on the line or not"""
        status = True if self.eq(other.x, other.y) == 0 else False
        if status:
            msg = "The point ({}, {}) is on the line.".format(other.x, other.y)
        else:
            distance = abs(self.eq(other.x, other.y)) / (self.coeff[0]**2 + self.coeff[1]**2)**0.5
            msg = "The point ({}, {}) is not on the line.\nDistance: {:.4f}".format(other.x, other.y, distance)
        return msg