OOP
'''
Class
Object/Instance
Object/Instance Variable
Class Attribute/Data member (Static variable)
Instance Methods
Class Methods (@classmethod)
Static Methods (@staticmethod)
Abstract Methods (a way)
Constructor (__init__)
Destructor (__del__)
@property
@attribute_name.setter
@attribute_name.deleter
Instance
Instantiation
self -> instance reference
cls -> class reference
super() -> parent class reference
Inner Classes
'''
# The need for "self"
class Car:
pass
obj = Car() #Insantiation, Instance = obj
obj.brand = 'Audi'
obj.type = 'Sports'
obj.price = 5000000
print(obj.brand, obj.type, obj.price) # => Audi Sports 5000000
# We can 'automate' this variable creation using "self" to refer to calling object
class Car:
def __init__(self):
self.brand = 'Audi'
self.type = 'Sports'
self.price = 5000000
obj = Car()
print(obj.brand, obj.type, obj.price) # => Audi Sports 5000000
# First parameter is always the instance of the calling object (i.e. self), it is passed implicitly
# Variables (Class vars and Instance vars)
class Car:
wheels = 4 # class variable
def __init__(self, br):
self.brand = br #instance variables
self.year = 1999
def foobar(self):
print(self.year)
obj = Car('BMW')
# Instance Methods = Calling with two visually different but functionally excatly the same syntaxes
obj.foobar() # => 1999
Car.foobar(obj) # => 1999
# Class attribute "wheels" can be accessed by both class name and via object
print(Car.wheels) # => 4
print(obj.wheels) # => 4
# Changing values of class attribute
class Fun:
name = 'Arya'
foo = Fun()
print(foo.name) # "Arya"
bar = Fun()
Fun.name = 'Abhi' # shared variable modified here
print(bar.name) # "Abhi"
# Class Methods
class A:
@classmethod # required to access via A.foobar()
def foobar(cls): #can also use self insted of cls, it has first parameter as classname implicitly unlike staticmethods which have no object params
print('foobar')
obj = A()
# Can be accessed by both class name and object just like class attributes
A.foobar() # => foobar
obj.foobar() #=> foobar
# Static methods
class A:
@staticmethod
def foobar(): #not supposed to have any class or object instance arguments
print('foobar')
obj = A()
# Can be accessed by both class name and object just like class attributes
A.foobar() # => foobar
obj.foobar() #=> foobar
# Abstract methods can be implemented like so
class Pizza(object):
def get_radius(self):
raise NotImplementedError
# Inner Classes -> Classes inside other classes
class A:
name = 'Apple'
def __init__(self):
self.objB = self.B() #can create object of inner class in outer class
class B: #inner class
name = 'Ball'
objA = A()
print(objA.objB.name) # => Ball
#creating object of B outside A
anotherObjB = A.B()
print(anotherObjB.name) # => Ball
# Inheritence
'''
Single
Mutli-Level
Multiple
Constructors in Inheritence
Method Resolution Order(MRO) [Left --> Right]
'''
"""
Superclass
|
Subclass
Subclass can access all vars and methods of super but not vice-versa.
"""
# Single Inheritence
class A:
pass
class B(A):
pass
# Multi-level Inheritence
class A:
pass
class B(A):
pass
class C(B):
pass
# Multiple Inheritence
class A:
pass
class B:
pass
class C(A,B):
pass
# Constructor behaviour in Inheritence -> By default only the calling object's class __init__ is called
class A:
def __init__(self):
print('Const of A')
class B(A):
def __init__(self):
print('Const of B')
obj = B() # => Const of B
# If we want to call __init__ of A too, use "super()""
class A:
def __init__(self):
print('Const of A')
class B(A):
def __init__(self):
super().__init__() #redirect to const of A first
print('Const of B')
obj = B()
'''
Const of A
Const of B
'''
# In multiple inheritence -> we can't use super(), can we? Yes, left to right order is followed, i.e. if
class C(A, B):
pass
#then only the constructor of A is called with super() call
#this applies to other class and object methods too and is called Method Resolution Order (MRO)
# Polymorphism - 4 ways in Python
'''
Duck Typing
Operator Overloading
Method Overloading
Method Overiding
'''
# Duck Typing -> Can have many diffrent classes and their objects can be passed to functions accessing a common method that all of them have.
class A:
my_attr = 'Anything'
def foo(self):
print('foobar')
class B:
def foo(self):
print('boofar')
objA = A()
objB = B()
def printMe(obj): #external method
obj.foo()
printMe(objA) # don't care if we are passing any class object as long as it has foo() method
printMe(objB)
# Operator Overloading -> Customizing(overloading) default built-in operators for performing operations on class objects
# Default operators' built-in actions:
a = 3
b= 4
print(a + b) #int.__add__(a, b)
print(a - b) #int.__sub__(a, b)
print(a * b) #int.__mul__(a, b)
print(a > b) #int.__gt__(a, b)
# Create methods that overload these in class and you can use +, -, *, >, etc..
class A:
marks = 40
def __add__(self, any_var):
return self.marks + any_var.marks
class B:
marks = 50
objA = A()
objB = B()
print(objA + objB) # => 90, not an error
# Method Overloading (can't create two functions with same name in same scope in Python)
# Use *vargs or default arguments for this
# Method Overriding
class A:
def show(self):
print('A')
class B(A):
def show(self):
print('B')
objB = B()
objB.show() # => B
#B.show() overrides A.show()
# Advanced OOPS
'''
Name Mangling: if we have double underscore as prefix to a identifier name in a class then
it is implicitly converted by the interpreter as "_Classname__name".
So if we make a method name as "__name", no one will be able to call it with that name. but they can with "_Classname__name"
Link: https://www.geeksforgeeks.org/name-mangling-in-python/
'''
class Student:
def __init__(self, name):
self.__name = name
def displayName(self):
print(self.__name)
s1 = Student("foobar")
s1.displayName()
# Raises an error
print(s1.__name)
# Printing Objects
'''
__str__
__repr__
Add these to the class to make objects printable, both return a String literal.
'''
class Test:
def __init__(self, a, b):
self.a = a
self.b = b
def __repr__(self):
return f"Test a: {self.a} b: {self.b}"
def __str__(self):
return f"From str method of Test: a is {self.a}, b is {self.b}"
t = Test(123, 567)
print(t) # This calls __str__(), if no __str__ is present then print(t) uses __repr__
print([t]) # This calls __repr__()
'''Output:
From str method of Test: a is 123, b is 567
[Test a: 123 b: 567]
'''
# User-defined Exception Class
'''
Derived from "Exception" class.
'''
class MyExcp(Exception):
def __init__(self, value):
self.value = value
def __str__(self):
return f"Exception with value {self.value} has occured."
try:
raise MyExcp(4)
except MyExcp as e:
print(e)
#Output: "Exception with value 4 has occured.""