Skip to main content

0403 | Object Oriented Programming

Introduction

  • Object Oriented Programming (OOP)| General
    • up until now | procedural programming
      • data and functions are mostly decoupled
    • Object Oriented Programming groups variables and methods
    • Structure software into reusable blueprints (classes)
    • Blueprint templates can create objects (instantiation)
    • Classes contain data (attributes)
    • Class contain functions (methods)
  • Examples
    • A Person class
      • Name as an attribute
      • Say name as a method
    • Bob is a person object with a name which can be said
    • Alice is a person object with a name which can be said
    • Bob is NOT Alice
  • OOP | Benefits
    • Model and group complex data in reusable way
    • Leverage existing structures (inheritance)
    • Enables class-specific behaviour (polymorphism)
    • Secure and protect attributes and methods (encapsulation)
    • Extendible and modular (overloading)

Classes, Objects and Method

  • Class attributes | defined directly in the class and shared by all objects of that class
    • changes made to the class attribute will change for all instances of the class
    • but changing the class attribute value of an instance will not change the value for the class or the other instances
  • demo-example | "classes_objects_methods.py" | (play around with commenting/uncommenting as you see fit)
    class Person:
    # class documentation
    'Person base class'

    # class attribute
    wants_to_hack = True

    # constructor -- to initialize an instance of the object
    # self -- reference to the object itself -- here:Person
    # invoked automatically whenever a new object of the class is instantiated
    def __init__(self, name, age):
    self.name = name
    self.age = age

    def print_name(self):
    print("My name is {}".format(self.name))

    def print_age(self):
    print("My age is {}".format(self.age))

    def birthday(self):
    self.age += 1

    print("-"*50)
    ## --------------- OBJECTS --------------- ##

    # creating instances of the same class
    bob = Person("bob", 30)
    alice = Person("alice", 20)
    mallory = Person("mallory", 50)

    # objects -- __main__.Person
    print(bob)
    print(alice)
    print(mallory)


    print("-"*50)
    ## --------------- METHODS AND ATTRIBUTES --------------- ##

    # using the methods
    bob.print_name()
    bob.print_age()
    alice.print_name()
    alice.print_age()
    mallory.print_name()
    mallory.print_age()

    # changing attribute values
    bob.age = 31
    bob.print_age()
    bob.birthday()
    bob.print_age()
    bob.birthday()
    bob.print_age()

    print(bob.name)
    print(bob.age)

    # __main__.Person
    print(type(bob))

    print("-"*50)
    ## --------------- CLASS SPECIFIC FUNCTIONS --------------- ##

    # class specific function -- check if attribute exists
    print(hasattr(bob, "age"))
    print(hasattr(bob, "asd"))

    # class specific function -- check for object values
    print(getattr(bob, "age"))

    # class specific function -- set attributes for an object
    # if attribute does NOT exist --> it will create it
    setattr(bob, "asd", 100)
    print(getattr(bob, "asd"))

    # class specific function -- delete attributes for an object
    print(hasattr(bob, "asd")) # True
    delattr(bob, "asd")
    # no longer exists
    print(hasattr(bob, "asd")) # False

    print("-"*50)
    ## --------------- CLASS ATTRIBUTES --------------- ##
    # defined directly in the class and shared by all objects of that class

    print(Person.wants_to_hack)
    print(bob.wants_to_hack)
    print(alice.wants_to_hack)
    print(mallory.wants_to_hack)

    print("-"*5)
    # changes made to the class attribute will change for all instances of the class
    Person.wants_to_hack = "No way!"
    print(Person.wants_to_hack)
    print(bob.wants_to_hack)
    print(alice.wants_to_hack)
    print(mallory.wants_to_hack)

    print("-"*5)
    # but changing the class attribute value of an instance will not change the
    # value for the class or the other instances
    bob.wants_to_hack = "Yes way!"
    print(Person.wants_to_hack)
    print(bob.wants_to_hack) # only bob's value is changed
    print(alice.wants_to_hack)
    print(mallory.wants_to_hack)

    print("-"*50)
    ## --------------- DELETING ATTRIBUTES --------------- ##
    # it is possible to delete attributes, objects or the entire class itself with `del`

    # delete an attribute
    bob.print_name()
    del bob.name
    print(hasattr(bob, "name"))

    # delete a class
    # del Person
    # # we can still access it
    # print(alice.name)

    # but creating new instances is no longer possible -- NameError
    bob2 = Person("bob2", 35)

    print("-"*50)
    ## --------------- BUILT-IN CLASS ATTRIBUTES --------------- ##
    # built-in class attributes associated with ALL python classes

    # dictionary containing the classes namespace
    print(Person.__dict__)
    # the class documentation string
    print(Person.__doc__)
    # the class name
    print(Person.__name__)
    # the module name in which the class is defined -- main in interactive mode
    print(Person.__module__)

Inheritance

  • General | a way of creating a new class by using details of an already existing class, but without needing to make any changes to that existing class
    • the new class | derived class | child class
    • the existing class | base class | parent class
  • demo-example | "inheritance_demo.py" | (play around with commenting/uncommenting as you see fit)
    ## --------------- PARENT CLASS --------------- ##
    class Person:
    # class documentation
    'Person base class'

    # class attribute
    wants_to_hack = True

    # constructor -- to initialize an instance of the object
    # self -- reference to the object itself -- here:Person
    # invoked automatically whenever a new object of the class is instantiated
    def __init__(self, name, age):
    self.name = name
    self.age = age

    def print_name(self):
    print("My name is {}".format(self.name))

    def print_age(self):
    print("My age is {}".format(self.age))

    def birthday(self):
    self.age += 1

    ## --------------- CHILD CLASS --------------- ##
    class Hacker(Person):
    def __init__(self, name, age, cves):
    super().__init__(name, age)
    self.cves = cves

    def print_name(self):
    print("My name is {} and I have {} CVEs".format(self.name, self.cves))

    def total_cves(self):
    return self.cves

    ## --------------- INSTANTIATION --------------- ##
    bob = Person("bob", 30)
    alice = Hacker("alice", 20, 5)

    bob.print_name()
    alice.print_name()

    print(bob.age)
    print(alice.age)

    bob.birthday()
    alice.birthday()

    print(bob.age)
    print(alice.age)

    # bob has no cve attribute
    print(alice.total_cves())
    print(hasattr(bob, "cves"))

    print("-"*5)
    ## --------------- FUNCTIONS RELATED TO CLASS RELATIONSHIPS --------------- ##
    print(issubclass(Hacker, Person))
    print(issubclass(Person, Hacker))

    # true if instance of the class or subclass
    print("-"*5)
    print(isinstance(bob, Person))
    print(isinstance(bob, Hacker))

    print(isinstance(alice, Person))
    print(isinstance(alice, Hacker))

Encapsulation

  • General | to prevent accidental modification to data within a class instance
    • but should NOT rely on it for security! | could use dict to grab the data anyway
      • once you know the attribute name, you can directly change it | bob._Person__age = 50
  • restricting direct access to attributes and methods
  • by default, all methods and variables of a python class are public
  • setting an attribute private | prefix name with __ | self.__name
  • accessing private attributes is done via setter/getter methods
  • demo-example | "encapsulation_demo.py" | (play around with commenting/uncommenting as you see fit)
    class Person:
    # class documentation
    'Person base class'

    # class attribute
    wants_to_hack = True

    # constructor -- to initialize an instance of the object
    # self -- reference to the object itself -- here:Person
    # invoked automatically whenever a new object of the class is instantiated
    def __init__(self, name, age):
    self.name = name
    self.__age = age

    def print_name(self):
    print("My name is {}".format(self.name))

    # getter method for age
    def get_age(self):
    return self.__age

    # setter method for age
    def set_age(self, age):
    self.__age = age

    def print_age(self):
    print("My age is {}".format(self.__age))

    def birthday(self):
    self.__age += 1


    ## --------------- ACCESSING PRIVATE ATTRIBUTES --------------- ##
    bob = Person("age", 30)

    # once set private -- it will no longer work
    # print(bob.age)
    # nor will this
    # print(bob.__age)

    # access it via getter methods
    print(bob.get_age())
    bob.set_age(31)
    print(bob.get_age())
    bob.birthday()
    print(bob.get_age())

    ## --------------- GETTING AROUND ENCAPSULATION --------------- ##
    # grab masked private data
    print(bob.__dict__)

    # change data
    bob._Person__age = 50

    # grab masked private data
    print(bob.__dict__)

Polymorphism

  • General | the ability to use a common interface for multiple, different types
  • use the exact same function even if we are passing different types to that function
  • demo-example | "polymorphism_demo.py" | (play around with commenting/uncommenting as you see fit)
    ## --------------- PARENT CLASS --------------- ##
    class Person:
    # class documentation
    'Person base class'

    # class attribute
    wants_to_hack = True

    # constructor -- to initialize an instance of the object
    # self -- reference to the object itself -- here:Person
    # invoked automatically whenever a new object of the class is instantiated
    def __init__(self, name, age):
    self.name = name
    self.age = age

    def print_name(self):
    print("My name is {}".format(self.name))

    def print_age(self):
    print("My age is {}".format(self.age))

    def birthday(self):
    self.age += 1

    ## --------------- CHILD CLASS --------------- ##
    class Hacker(Person):
    def __init__(self, name, age, cves):
    super().__init__(name, age)
    self.cves = cves

    def print_name(self):
    print("My name is {} and I have {} CVEs".format(self.name, self.cves))

    def total_cves(self):
    return self.cves


    print("-"*50)
    ## --------------- POLYMORPHISM -- BUILT-IN EXAMPLES --------------- ##

    # the same function 'len' but with different types -- string vs list
    print(len("string"))
    print(len(['l','i','s','t']))


    print("-"*50)
    ## --------------- POLYMORPHISM -- CLASS METHOD EXAMPLES --------------- ##
    bob = Person("bob", 30)
    alice = Hacker("alice", 25, 10)
    people = [bob, alice]

    for person in people:
    person.print_name()
    print(type(person))


    print("-"*50)
    ## --------------- POLYMORPHISM -- ANY OBJECT --------------- ##
    def obj_dump(object):
    object.print_name()
    print(object.age)
    object.birthday()
    print(object.age)
    print(object.__class__)
    print(object.__class__.__name__)


    obj_dump(bob)
    obj_dump(alice)

Operator Overloading

  • General | changing the meaning of an operator depending on the operands used
  • demo-example | "operator_overloading_demo.py" | (play around with commenting/uncommenting as you see fit)
    class Person:
    # class documentation
    'Person base class'

    # class attribute
    wants_to_hack = True

    # constructor -- to initialize an instance of the object
    # self -- reference to the object itself -- here:Person
    # invoked automatically whenever a new object of the class is instantiated
    def __init__(self, name, age):
    self.name = name
    self.age = age

    def print_name(self):
    print("My name is {}".format(self.name))

    def print_age(self):
    print("My age is {}".format(self.age))

    def birthday(self):
    self.age += 1

    # overload the str built in method
    def __str__(self):
    return "My name is {} an I am {} years old.".format(self.name, self.age)

    # implement the built in addition operator
    # other is expected to be an other instance of the class,
    # but not the same instance which is calling the method
    def __add__(self, other):
    return self.age + other.age

    # overloading the less than operator
    def __lt__(self, other):
    return self.age < other.age


    print("-"*50)
    ## --------------- OPERATOR OVERLOADING -- BUILT-IN EXAMPLES --------------- ##
    print(1 + 1)
    print("1" + "1")

    print("-"*50)
    ## --------------- OPERATOR OVERLOADING -- CLASS EXAMPLES --------------- ##
    bob = Person("bob", 30)
    alice = Person("alice", 35)

    print("-"*5)
    # call overloaded __str__ method
    print(bob)

    print("-"*5)
    # try adding them together
    # will not work without overwriting/implementing the __add__ method
    print(bob + alice)
    print(alice + bob)

    print("-"*5)
    # overloaded lt operator
    print(bob < alice)
    print(alice < bob)

Class Decorators

  • property decorator | to declare a method as a property object of the class | @property
  • class method decorator | python syntactic sugar | @classmethod
    • bound to a class, rather than it's object
    • does NOT require a creation of a class instance
    • can only access class method attributes, not the per instance attributes
    • first attribute | cls | to access class attributes
  • class method decorator | factory methods
    • use class methods as a type of factory to create instances of the class
  • static method decorator | @staticmethod
    • to define a static method in the class
    • can NOT access the class attributes or the per instance attributes
    • does not require a class instance
    • can be called by both and the instances of the class
    • takes no parameters | not even self | does not know anything about the class
    • useful when we do not want some subclass to overwrite some method implementation
  • demo-example | "class_decorators_demo.py" | (play around with commenting/uncommenting as you see fit)
    class Person:
    # class documentation
    'Person base class'

    # class attribute
    wants_to_hack = True

    # constructor -- to initialize an instance of the object
    # self -- reference to the object itself -- here:Person
    # invoked automatically whenever a new object of the class is instantiated
    def __init__(self, name, age):
    self.name = name
    self.__age = age

    def print_name(self):
    print("My name is {}".format(self.name))

    # getter method for age
    def get_age(self):
    return self.__age

    # property decorator -- getter method
    @property
    def age(self):
    return self.__age

    # property decorator -- setter method
    @age.setter
    def age(self, age):
    self.__age = age

    # property decorator -- delete method
    @age.deleter
    def age(self):
    del self.__age

    # class method decorator -- `cls` for accessing class attributes
    @classmethod
    def wants_to(cls):
    return cls.wants_to_hack

    # class method decorator -- bob factory
    @classmethod
    def bob_factory(cls):
    return cls("bob", 30)

    # static method -- no parameters
    @staticmethod
    def static_print():
    print("I am the same!")

    # setter method for age
    def set_age(self, age):
    self.__age = age

    def print_age(self):
    print("My age is {}".format(self.__age))

    def birthday(self):
    self.__age += 1


    print("-"*50)
    ## --------------- CLASS DECORATORS -- PROPERTY DECORATOR --------------- ##
    bob = Person("bob", 30)

    # will not work without a property decorator
    print(bob.age)

    print("-"*5)

    # setter test
    bob.age = 50
    print(bob.age)

    # deleter test
    # del bob.age
    # print(hasattr(bob, "age")) # false

    print("-"*50)
    ## --------------- CLASS DECORATORS -- CLASS METHOD DECORATOR --------------- ##
    print(Person.wants_to())

    print("-"*5)
    # create instances of bob with the factory
    bob1 = Person.bob_factory()
    bob2 = Person.bob_factory()
    bob3 = Person.bob_factory()

    bob1.print_name()
    bob2.print_name()
    bob3.print_name()

    print("-"*50)
    ## --------------- CLASS DECORATORS -- STATIC METHOD DECORATOR --------------- ##
    Person.static_print()
    bob1.static_print()
    bob2.static_print()
    bob3.static_print()