Skip to content

Object-Oriented Programming

Object-Oriented Programming (OOP) in Python

  • Everything in Python is an Object
  • Shows data types through class type
print(type(None))     # <class 'NoneType'>
print(type(True))     # <class 'bool'>
print(type(5))        # <class 'int'>
print(type(5.5))      # <class 'float'>
print(type('hello'))  # <class 'str'>
print(type([]))       # <class 'list'>
print(type(()))       # <class 'tuple'>
print(type({}))       # <class 'dict'>
  • Each built-in data type in Python is actually a class.
  • When you create a value, you’re instantiating an object of that class.
  • Example: 5 is an object of class int, 'hello' is an object of class str.

Creating Your Own Classes

  • We're able to create our own types, our own data types with different attributes and methods.
  • We can create our own classes and objects.
  • OOP is a programming paradigm that uses "objects" to represent data and methods to manipulate that data.
  • A class is a blueprint for creating objects. It defines a set of attributes and methods.
  • An object is an instance of a class. It is created based on the blueprint provided by the class and has its own unique state and behavior.
class BigObj:           # Blueprint (class name should be in PascalCase, singular)
    pass

obj1 = BigObj()         # () Instantiate or create an object

print(type(obj1))       # shows that BigObj is a class
  • This is how you want to organize your code.
  • Thinking less procedural and thinking more in terms of functionality
  • Whenever we create a method the self parameter is required self refers to whatever's to the left of the dot.
class PlayerCharacter:          #class object attribute, it's not dynamic, it's static
    membership = True

    #Constructor or # Initialization or init method(dunder method)
    def __init__(self, name='anonymous', age=0):
        #Can access class object attribute through self, can add safeguards and controls
        if age > 18:
            self.name = name
            self.age = age

    def run(self):
        print('run')
        return 'done'

    def shout(self):
        print(f'my name is {self.name}')
@classmethod
  • It is to define a method that is bound to the class and not the object of the class.
  • it's a method on an actual class
  • Alternative constructor that can create objects in different ways
    @classmethod
    def add_things(cls, num1, num2):
        return cls('Teddy', num1 + num2)
@staticmethod
  • It is a method that doesn't operate on an instance nor modify class state.
  • It behaves like a regular function but belongs to the class's namespace.
    @staticmethod
    def add_numbers(num1, num2):
        return num1 + num2
init method
  • This is going to call the init method, or Object constructor
player1 = PlayerCharacter('Cindy', 20)
player1.attack = 50         # can add new attributes to the object after its been created
                            # can create different players with different attributes

print(player1)                          # Output: <__main__.PlayerCharacter object at 0x7f9c8c4d1d60>
print(player1.name)                     # Output: Cindy(access by .name)
print(player1.age)

player1.run()                           # Output: run
print(help(player1))                    # shows blueprint of the object
player1.shout()                         # Output: my name is Cindy

player2 = PlayerCharacter.add_things(12, 10)
print(player2.age)
Another Example: Cat Class
class Cat:
    def __init__(self, name, color):
        self.name = name
        self.color = color

    def meow(self):
        print(f'{self.name} says meow!')


cat1 = Cat('Whiskers', 'black')
print(cat1.name)        # Output: Whiskers
print(cat1.color)       # Output: black
cat1.meow()             # Output: Whiskers says meow!

Syntax of OOP

class NameOfClass:
    def __init__(self, param1, param2):
        self.param1 = param1
        self.param2 = param2

    def method(self):
        code
        pass

    @classmethod
    def class_method(cls, param1, param2):
        code
        pass

    @staticmethod
    def static_method(param1, param2):
        code
        pass

4 Pillars of OOP

1. Encapsulation :

  • It is the binding of data and functions that manipulate that data and we encapsulate into one big object{OOP}
  • It keeps methods, functions and all other stuff safe from outside interference and misuse.

2. Abstraction :

  • It is the concept of hiding the complex reality while exposing only the necessary parts.
  • The idea behind abstraction is that we hide away information and only give access to things that a user is concerned about. _name : protected attribute, underscore means that you shouldn't touch this __name : private attribute, double underscore means that you really shouldn't touch this

3. Inheritance :

  • It is a way to form new classes using classes that have already been defined. In python, everything is an object, and all objects inherit from a base object class.

Example of Inheritence:

class User(object):        # parent class or base class or super class
  • The object is default parent class in python comes with built-in dundermethods
  • Python inherits from the base object class that Python provides we don't need init method if no variables or attributes are to be assigned
    def __init__(self, email):
        self.email = email

    def sign_in(self):     # chilren classes, subclasses, or derived classes
        print('logged in')
class Wizard(User):                 # Wizard class inherits User class by passing it as parameter
    def __init__(self, name, power, email):
        super().__init__(email)     # calling the parent class constructor
        self.name = name
        self.power = power

    def attack(self):
        print(f'attacking with power of {self.power}')


class Archer(User):
    def __init__(self, name, num_arrows):
        self.name = name
        self.num_arrows = num_arrows

    def attack(self):
        print(f'attacking with arrows: arrows left - {self.num_arrows}')

    def run(self):
        print('ran really fast')


wizard1 = Wizard('Merlin', 50, 'merlin@gmail.com')
archer1 = Archer('Robin', 100)

wizard1.attack()          # Output: attacking with power of 50
archer1.attack()          # Output: attacking with arrows: arrows left - 100

wizard1.sign_in()         # Output: logged in # inherited method from User class
print(wizard1.email)      # inherited attribute from User class
isinstance

Check if something is an instance or object of a class - syntax = isinstance(instance, ClassName) # returns True or False

print(isinstance(wizard1, Wizard))   # Output: True
print(isinstance(wizard1, User))     # Output: True
print(isinstance(wizard1, object))   # Output: True(all classes inherit from object class)
Multuple Inheritance

This means that a class can be derived from more than one base class.

class HybridBorg(Wizard, Archer):  # can give multiple parameters
    def __init__(self, name, arrows):
        Archer.__init__(self, name, arrows)


hb1 = HybridBorg('borgie', 100)
print(hb1.sign_in())
print(hb1.num_arrows)
print(hb1.run())

4. Polymorphism :

  • means having many forms.
  • Same method name, different behavior depending on the object.
print(wizard1.attack())
print(archer1.attack())

They share the same method names but the object that's calling is different

Example of Polymorphism:

def player_attack(char):
    char.attack()

player_attack(wizard1)
player_attack(archer1)

for char in [wizard1, archer1]:
    char.attack()

Introspection

Introspection in computer programming means the ability to determine the type of an object at runtime.(shows what you have access to)

print(dir(wizard1))   # shows all attributes and methods of the object

Dunder Method(Double Underscore Method) / Magic Method

Dunder methods are special methods in Python that start and end with double underscores (e.g., str, len). - Used to customize object behavior.

class Toy():
    def __init__(self, color, age):
        self.color = color
        self.age = age
        self.my_dict = {'name': 'Yoyo', 'has_toys': True}

The str method is only modified when we use it on this specific object.

    def __str__(self):
        return f'{self.color}'

() or call method allows call functions is using this dunder call.

    def __call__(self):
        return 'yes?'
    def __len__(self):
        return 5

    def __getitem__(self, i):
        return self.my_dict[i]

    def __del__(self):
        print('deleted!')
action_figure = Toy('red', 0)

print(action_figure.__str__())  # default dunder method that returns a string representation
print(str(action_figure))       # same as above

print(len(action_figure))       # Output: 5
print(action_figure())          # Output: yes?
print(action_figure['name'])    # Output: Yoyo

del action_figure              # Output: deleted!

Method Resolution Order (MRO)

  • Defines the order Python searches for methods in inheritance hierarchies.

  • Rule that Python follows to determine when you run method having complicated inheritance structure.

class A:
    num = 10

class B(A):
    pass

class C(A):
    num = 1

class D(B, C):
    pass

print(D.num)                # Output: 1

Python looks for num in D, then B, then C, then A. It finds num in C first.

print(D.mro())              # shows the method resolution order
print(D.__mro__)            # same as above
D.__str__                   # shows the dunder methods available for class D

Explore different use cases of MRO:
Use case of MRO