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)
- up until now | procedural programming
- 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
- A Person class
- 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
- once you know the attribute name, you can directly change it |
- but should NOT rely on it for security! | could use
- 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()