As we may know, programming languages can be divided in two different paradigms, Procedure-Oriented and Object-oriented. Early computer programming was based on a procedure-oriented approach, where problems could be solved by designing algorithms. C is a classical procedure-oriented programming language. But with the continuous improvement of computer technology, computers are being used to solve increasingly complex problems. Therefore, objected-oriecnted was developed. Through object-oriented methods, it is more conducive to analyzing, designing, and programming complex systems in a way that is understandable to humans.
Characteristics of OOP
- Objects and Classes
The idea of OOP is to focus on the “objects” our program will deal with. Objects, like tangible entities in the real world, are instances of classes. Classes, on the other hand, serve as blueprints, defining the properties and behaviors of objects. In OOP, we can make new “class” definitions for whatever types of objects we want our codes to deal with. - Encapsulation
In python, classes have “attributes” and “methods”. The “attributes” are like the variables that are tied to the class. The “methods” are essentially functions that are tied to the class. Encapsulation involves bundling attributes and methods within a class. By encapsulating related functionalities, developers can create modular and maintainable code. It’s like packaging the inner workings of a class in a sealed box, exposing only what’s necessary. - Inheritance
Inheritance allows classes to inherit properties and behaviors from another class. A “subclass” is a new class that “inherits” from an existing class. The class it inherits from is called the “superclass”. Imagine a superclass “vehicle” with common attributes and methods, and subclasses like “car” and “motocycle” inheriting these traits while adding their unique features. - Polymorphism
Polymorphism, meaning ‘many forms,’ enables objects to take different forms based on the context. Method overloading and method overriding are common examples. For instance, a ‘Shape’ class may have a method ‘area,’ and subclasses like ‘Circle’ and ‘Rectangle’ can implement it differently.
Here we define a class. The basic format of a class definition:
class ClassName:
# everything indented here is the code that defines the class
# the "initialization" method or "constructor" method is the method every class needs to
# defind how to create an object of the class
def __init__(self, var_1, var_2):
self.attr_1 = var_1
self.attr_2 = var_2
def method_name(self):
# here we define a method of a class
# in the class definition, the object itself is called "self"
# every method in the class needs to have "self" in the argument
# when creating a instance of an object
obj_name = ClassName(var_1, var_2)The attributes and methods in class can be public, protected, and private. They are public by default, and can be accessed by “.” operator. The public attributes and methods can be accessed outside of the class. The protected ones can be accessed by the class and its subclasses. As a matter of principle, we shouldn’t access or modify the protected attributes outside the class. The private ones can only be accessed inside the class. In Python, a single underscore (“_”) is used for protected attributes and methods, and a double underscore (“__”) is used for private attributes and methods. As the principle of encapsulation, it’s a good habit to set the attributes and some helper methods to be private, so that they are harder to accidentally use the wrong procedure on the wrong data.
# defind a class
class MyClass:
def __init__(self, var_1, var_2):
# public attribute
self.pub_attr1 = var_1
# private attribute
self.__pri_attr2 = var_2
def __pri_method(self):
print("this is a private method")
def pub_method(self):
print("this is a public method")
# create a instance
my_class = MyClass(12, 5)
# access the attributes
print(my_class.pub_attr1) # it will print 12
print(my_class.__pri_attr2) # it will raise a "AttributeError"
# use the method
my_class.pub_method() # it will print out "this is a public method"
my_class.__pri_method() # it will raise a "AttributeError"When using a dot operator outside the class and trying to modify to private attributes, the changes will not apply to the attributes inside the class. Instead of accessing the attributes directly using dot (“.”) operator, we can use a setter/getter to modify or access them.
class Car:
def __init__(self, new_brand, new_year):
# private attribute
self.__brand = new_brand
self.__year = new_year
# a setter for brand
def set_brand(self, new_brand):
self.__brand = new_brand
# a setter for year
def set_year(self, new_year):
self.__year = new_year
# a getter for brand
def get_brand(self):
return self.__brand
# a getter for year
def get_year(self):
return self.__year
# create an instance
car = Car("Honda", 2017)
# before changing
print("before changing:")
print(car.get_brand(), car.get_year())
# after changing
car.set_brand("Nissan")
car.set_year(2020)
print("after changing:")
print(car.get_brand(), car.get_year())
# result:
before changing:
Honda 2017
after changing:
Nissan 2020In Python, there are many “magic methods” that have a built-in name which we can use to cause objects of our class to have a certain way in certain situations. Different magic methods are tied to different behaviors. The “magic methods” have the double userscores (“__”) around their name. The idea is that we don’t call the method with its actual name, instead the method is called automatically in the right situation. The procedures of overloading the magic methods refer to the general principal of polymorphism.
Some “magic methods” of a class are:
- __init__: which is automatically called when creating an object using the class name, usually called constructor
- __repr__/__str__: which is automatically called when entering or printing an object
- __add__: which is automatically called when we try to add with “+”
- __mul__: which is automatically called when we try to multiply with “*”
- __eq__: which is automatically called when checking equality with “==”
- __int__: which is automatically called when converting to an integer using “int()”
- __contains__: which is automatically called when checking membership using “in”
# suppose we have a "Poly" class represents polynomials in the form
# a0 + a1*x + a2*x^2 + ... + an*x^n
class Poly:
def __init__(self, *args):
self.coeffs = args
# normal method: get the value of the polynomial at some x
def value_at(self, x):
T = self.coeffs
L = [T[i]*x**i for i in range(len(T))]
return sum(L)
# overload the method __str__
def __str__(self):
out_str = ""
T = self.coeffs
for i in range(len(T)):
if T[i] != 0:
out_str += f"({T[i]}*x^{i}) +"
return out_str
# overload the method __add__
def __add__(self, other):
if len(self.coeffs) < len(other.coeffs):
short = list(self.coeffs)
long = list(other.coeffs)
else:
short = list(other.coeffs)
long = list(self.coeffs)
for i in range(len(short)):
long[i] += short[i]
out_coeffs = tuple(long)
out_poly = Poly(*out_coeffs)
return out_polyAs mentioned before, a “subclass” can inherit all of the public and protected attributes (private attributes can not be inherited) and methods (including magic methods) from its “superclass”. On the other hand, a “subclass” can also has its unique features. Therefore, when creating classes of objects, we better abstract their common features if any and make it a superclass. Then let the derived class inherit this superclass instead of making many individual classes with some same features. This will help us reuse and manage our codes better. For instance, a quadratic and a monomial are special cases of polynomials. So we can make subclasses of quadratic and monomial.
# suppose a quadratic is in the form: ax^2 + bx + c
class Quad(Poly):
def __init__(self, a, b, c):
# call the __init__ method of superclass
super().__init__(c, b, a)
def find_roots(self):
(c, b, a) = self.coeffs
x1 = (-1 * b + (b ** 2 - 4 * a * c) ** 0.5) / (2 * a)
x2 = (-1 * b - (b ** 2 - 4 * a * c) ** 0.5) / (2 * a)
return x1, x2
# suppose a monomial is in the form: ax^r
class Mono(Poly):
def __init__(self, a, r):
L = [0] * r + [a]
super().__init__(*tuple(L))
def find_roots(self):
return 0
# creat instances
my_quad = Quad(3, -5, 4)
print(my_quad)
my_mono = Mono(3, 2)
print(my_mono)
# result
(4*x^0) +(-5*x^1) +(3*x^2) +
(3*x^2) +Small notes:
There are two built-in functions for “inspecting” objects of a class. The “var(obj)” function will show us the attributes and their values in dictionary format. The “dir(obj)” function will by default show us all the methods and attributes we can potentially access (including default magic methods that may not actually do anything useful).
