Object Oriented Programming¶

Author: Jacy Cui

  • Encapsulation

  • Inheritance

  • Polymorphism

Encapsulation¶

$$ \text{data abstraction} = \langle\text{objects}, \text{operations}\rangle $$

In [1]:
class Account:
    """
    Instance Attributes:

    balance: the money left in this account

    holder: the holder of this account

    Class Attributes:
    
    interest: the interest rate of all accounts
    """

    # class attribute
    interest = 0.02

    # constructor
    def __init__(self, account_holder): 
        """
        Creates an account with balance `0` and held by `account_holder`.
        """
        self.balance = 0 # instance attribute
        self.holder = account_holder
    
    # methods
    def deposit(self, amount): 
        """
        Deposits `amount` money into this account and returns the balance.
        """
        self.balance += amount
        return self.balance
    
    def withdraw(self, amount):
        """
        Withdraws `amount` money from this account if `balance` is greater
        than or equal to `amount` and returns the updated `balance`, otherwise
        returns 'Insufficient funds'.
        """
        if amount > self.balance:
            return 'Insufficient funds'
        self.balance -= amount
        return self.balance

jacy = Account('Jacy') # object construction

Evaluation of Dot Expression¶

<expression>.<name>
  1. Evaluatethe <expression> to the left of the dot, which returns the object of the dot expression.

  2. <name> is matched against the instance attributes of that object; if an attribute with that name exists, its value is returned.

  3. If not, <name> is looked up in the class, which yields a class attribute value (if no such class attribute exists, an AttributeError is reported).

  4. That value is returned unless it is a function, in which case a bound method is returned instead.

In [2]:
jacy.holder
Out[2]:
'Jacy'
In [3]:
jacy.balance
Out[3]:
0
In [4]:
jacy.deposit(10)
Out[4]:
10
In [5]:
jacy.withdraw(5)
Out[5]:
5

Specification¶

Developers often use existing code by specification:

class Account:
    """
    Instance Attributes:
    balance: the money left in this account
    holder: the holder of this account
    Class Attributes:
    interest: the interest rate of all accounts
    """

    def __init__(self, account_holder): 
        """
        Creates an account with balance `0` and held by `account_holder`.
        """
        pass
    
    def deposit(self, amount): 
        """
        Deposits `amount` money into this account and returns the balance.
        """
        pass
    
    def withdraw(self, amount):
        """
        Withdraws `amount` money from this account if `balance` is greater
        than or equal to `amount` and returns the updated `balance`, otherwise
        returns 'Insufficient funds'.
        """
        pass
In [6]:
Account
jjppp = Account('JJPPP')
jacy.deposit(10)
jacy.withdraw(5)
Account.interest
Out[6]:
0.02

OOP and Narural Language¶

  • Jacy eats an apple. => jact.eat(apple)

  • JJPPP is smart. => jjppp.is_smart() == True

  • Yinfeng dates. => yinfeng.dates()

Inheritance¶

In [7]:
class CheckingAccount(Account): # class NewClass(BaseClass1, BaseClass2, ...):
    withdraw_fee = 1
    interest = 0.01
    def withdraw(self, amount): # method overriding
        return Account.withdraw(self, amount + self.withdraw_fee)

class SavingsAccount(Account):
    deposit_fee = 2
    def deposit(self, amount): # method overriding
        return Account.deposit(self, amount - self.deposit_fee)

class CleverAccount(CheckingAccount, SavingsAccount):
    def __init__ (self, account_holder): # method overriding
        self.holder = account_holder
        self.balance = 1

Class Attributes Lookup¶

Base class attributes aren't copied into subclasses!

To look up a name in a class:

  1. If it names an attribute in the class, return the attribute value.

  2. Otherwise, look up the name in the base class, if there is one.

In [8]:
class A:
    z = -1
    def f(self, x):
        return B(x - 1)
    def __repr__(self):
        return 'A()'
    
class B(A):
    n = 4
    def __init__ (self, y):
        if y:
            self.z = self.f(y)
        else:
            self.z = C(y + 1)
    def __repr__(self):
        return f'B(z={self.z})'

class C(B):
    def f(self, x):
        return x
    def __repr__(self):
        return f'C(z={self.z})'

a = A()
b = B(1)
b.n = 5
In [9]:
C(2).n
Out[9]:
4
In [10]:
a.z == C.z
Out[10]:
True
In [11]:
a.z == b.z
Out[11]:
False
In [12]:
b
Out[12]:
B(z=B(z=C(z=1)))
In [13]:
b.z
Out[13]:
B(z=C(z=1))
In [14]:
b.z.z
Out[14]:
C(z=1)
In [15]:
b.z.z.z
Out[15]:
1
In [16]:
b.z.z.z.z
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[16], line 1
----> 1 b.z.z.z.z

AttributeError: 'int' object has no attribute 'z'

Liskov Substitution Principle¶

An object may be replaced by a subclass-object without breaking the program since inheritance represents an is-a relation.

In [17]:
class Dog:
    def __init__(self, name):
        self.name = name

    def make_noise(self):
        return 'Bark!'

class TinyDog(Dog):
    def make_noise(self):
        return 'Yip yip yip yip!'
    
class LargeDog(Dog):
    def make_noise(self):
        return 'Arooooooooooooooo!'

def kick_dog(dog):
    print('Me: Kick the dog!')
    print(dog.name, end='')
    print(f": {dog.make_noise()}")

yellow = Dog('BigYellow')
white = TinyDog('LittleWhite')
husky = LargeDog('Husky')
In [18]:
kick_dog(yellow)
Me: Kick the dog!
BigYellow: Bark!
In [19]:
kick_dog(white)
Me: Kick the dog!
LittleWhite: Yip yip yip yip!
In [20]:
kick_dog(husky)
Me: Kick the dog!
Husky: Arooooooooooooooo!
In [21]:
class EvilDog(Dog):
    def make_noise(self, bravery):
        return 'How dare you!!!'

evil = EvilDog('How dare you!!!')
In [22]:
kick_dog(evil)
Me: Kick the dog!
How dare you!!!
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[22], line 1
----> 1 kick_dog(evil)

Cell In[17], line 19, in kick_dog(dog)
     17 print('Me: Kick the dog!')
     18 print(dog.name, end='')
---> 19 print(f": {dog.make_noise()}")

TypeError: EvilDog.make_noise() missing 1 required positional argument: 'bravery'

Ploymorphism¶

  • Ad Hoc Polymorphism

    • e.g. Overloading
  • Parametric Polymorphism

    • e.g. Generic
  • Inclusion Polymorphism

    • e.g. Subtyping

Function Overloading¶

  • repr(x) behaves differently (i.e. polymorphically) when applied to different types of x.

    • String representation for the python interpreter.
  • str(x) behaves differently (i.e. polymorphically) when applied to different types of x.

    • String representation for the human interpreter.
In [23]:
class Link:
    empty = ()

    def __init__(self, first, rest=empty):
        assert rest is Link.empty or isinstance(rest, Link)
        self.first = first
        self.rest = rest

    def __repr__(self):
        if self.rest is not Link.empty:
            rest_repr = ', ' + repr(self.rest)
        else:
            rest_repr = ''
        return 'Link(' + repr(self.first) + rest_repr + ')'

    def __str__(self):
        string = '<'
        while self.rest is not Link.empty:
            string += str(self.first) + ' '
            self = self.rest
        return string + str(self.first) + '>'
    
s = Link(1, Link(2, Link(3)))
t = Link(-1, Link(-2, Link(-3)))
In [24]:
s
Out[24]:
Link(1, Link(2, Link(3)))
In [25]:
print(s)
<1 2 3>
In [26]:
t
Out[26]:
Link(-1, Link(-2, Link(-3)))
In [27]:
print(t)
<-1 -2 -3>
In [28]:
print(eval('Link(1, Link(2, Link(3)))'))
<1 2 3>
In [29]:
print(eval('<-1 -2 -3>'))
Traceback (most recent call last):

  File /opt/homebrew/Caskroom/miniconda/base/lib/python3.12/site-packages/IPython/core/interactiveshell.py:3508 in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)

  Cell In[29], line 1
    print(eval('<-1 -2 -3>'))

  File <string>:1
    <-1 -2 -3>
    ^
SyntaxError: invalid syntax

Operator Overloading¶

  • x + y is essentially x.__add__(y).

  • x == y is essentially x.__eq__(y).

In [30]:
class Link:
    empty = ()

    def __init__(self, first, rest=empty):
        assert rest is Link.empty or isinstance(rest, Link)
        self.first = first
        self.rest = rest

    def __repr__(self):
        if self.rest is not Link.empty:
            rest_repr = ', ' + repr(self.rest)
        else:
            rest_repr = ''
        return 'Link(' + repr(self.first) + rest_repr + ')'

    def __str__(self):
        string = '<'
        while self.rest is not Link.empty:
            string += str(self.first) + ' '
            self = self.rest
        return string + str(self.first) + '>'
    
    def __add__(self, other):
        if self is Link.empty or other is Link.empty:
            return Link.empty
        return Link(self.first + other.first, self.rest + other.rest)
    
    def __eq__(self, other):
        if self is Link.empty and other is Link.empty:
            return True
        if self is Link.empty or other is Link.empty:
            return False
        return self.first == other.first and self.rest == other.rest
    
s1 = Link(1, Link(2, Link(3)))
s2 = Link(1, Link(2, Link(3)))
t = Link(-1, Link(-2, Link(-3)))
In [31]:
s1 + s2
Out[31]:
Link(2, Link(4, Link(6)))
In [32]:
s1 + t
Out[32]:
Link(0, Link(0, Link(0)))
In [33]:
s1 is s2
Out[33]:
False
In [34]:
s1 == s2
Out[34]:
True
In [35]:
s1 is t
Out[35]:
False
In [36]:
s1 == t
Out[36]:
False
In [37]:
s1 is s1
Out[37]:
True
In [38]:
s1 == s1
Out[38]:
True
In [39]:
s1 + t == Link(0, Link(0, Link(0)))
Out[39]:
True