OOP concept
- OOP (Object-Oriented Programming) is a programming paradigm that organizes code into reusable objects, making it modular, easier to maintain, and more scalable.
- In OOP, we define classes, which serve as blueprints for creating objects. Objects represent real-world entities or concepts.
- A class defines the structure and behavior of these objects, serving as a template.
- An instance of a class is simply an object created from that class that represents it (a variable the class object,
ClassName()
, is assigned to). A class can have many instances (e.g., aUser
class can have many real user instances filled with personal data). Their names do not matter. - Attributes (also called fields or properties) are variables associated with an object, representing the state or characteristics of the object. Their values differ between instances (e.g., the age varies by user but is still the same type of numerical value).
- Inside classes, we define methods, which are functions that determine the behavior of objects. Methods are invoked using an object of the class they belong to (an instance).
- Once an instance is created, we can access and modify the class's attributes and call its methods through it. Every word a class name consists of should start with an uppercase letter (Pascal case -
ClassName
). - In Python, classes are also objects (everything is an object).
Abstraction
Abstraction is a process that deals with complexity by hiding unnecessary information from the user. It simplifies reality by creating a version of an object that consists only of essential information. Data is visible only to semantically related functions to prevent misuse. Example: We can do many things on a computer, but it doesn't show us how it's doing them because we don't need that information. The implementation parts are hidden. It is the first concept of object-oriented programming.
A constructor (__init__()
) is a method called by the compiler when an object of the class is created. A destructor (__del__()
) is called by the compiler when the object is destroyed. In Python, they are not mandatory (the default constructor is executed anyway). Besides, the destructor isn't called automatically and is rarely used. The constructor is commonly used for setting the initial values of variables. These two methods never feature the return
instruction. Methods can take arguments. If a constructor takes them, we give them while assigning the class to a variable, and in other words - creating its instance (e.g., x = Human(5)
).
Every method has a mandatory argument - self
, which isn't passed during its call. The self
keyword represents the class it is used in and allows to address all objects of the class at any point in the class. A field is a variable of any type that is declared in one of the methods of the class and is its object (has the self
keyword before it - self.x
). self.x
and x
are different variables. The former is an object belonging to a class (a field), and the latter is just a variable.
In a class, the order of methods and other objects is irrelevant because all objects have access to the whole class the whole time. Each class should be placed in a separate file.
A getter is a method that returns values outside of a class, and a setter is a method that edits an object belonging to a class. We should use them because, inside them, we can add, e.g., validation rules.
We can use the fields created in the __init__()
method anywhere inside the given class. The same goes for the other methods, but they must be called first (we cannot use a variable created in a method that hasn't been called yet). All the self
variables (fields) should (but don't have to) be created first in the __init__()
method, even if their value would initially be None
so that they are always accessible everywhere inside the class (they should be "declared" in __init__()
).
class Human:
def __init__(self):
self.age = 0 # a field (an instance variable - its value differs by instance)
print("Constructor")
def __del__(self):
print("Destructor")
def getAge(self):
return self.age
def setAge(self, age):
self.age = age
h = Human() # an instance of a class
h.age = 20
h.setAge(10)
print(h.getAge())
del h # calling the destructor
Encapsulation
Encapsulation prevents external code from being concerned with the internal workings of an object. It involves hiding the fields and methods of a class so that they are not accessible from the outside but only inside the class. The difference between abstraction and encapsulation is that abstraction hides objects and processes to show only the result at the end because it is the only thing that matters, while encapsulation hides them and blocks access to them, e.g., because some data should not be changed directly without a setter with validation rules inside. Encapsulation also promotes the creation of small, specialized methods and classes to keep them easy to understand, maintain, and reuse. It is the second concept of object-oriented programming. We should always identify the part of the code that changes and, when possible, encapsulate it.
If there is a __
before a field or a method name, it is a private attribute that cannot be called outside its class. Encapsulation blocks the possibility of bypassing certain instructions by, for example, editing the output variable outside the class. Most objects should be private, and outside of the class, they should be edited only through methods of this class (setters). If we try to access or change the value of a private object outside the class, we will get an error. The process of making a class attribute private is called mangling.
We can also write _
before a field, a method, or a class name. It means a protected attribute, which is ONLY A SUGGESTION to other programmers (it is a convention). In other languages, protected objects are private, except for the class's subclasses (I will explain this in a lesson about inheritance). These underscores are called access modifiers.
Functions and variables outside classes can also be made private (we wouldn't be able to access them from another file).
class Bank:
def __init__(self):
self.__balance = 1000 # a private field
def getBalance(self):
return self.__balance
def __setBalance(self, balance): # a private method
self.__balance = balance
def withdraw(self, howMuch):
if self.__balance < howMuch:
return False
else:
self.__setBalance(self.__balance - howMuch)
return True
b = Bank()
print(b.getBalance())
b.withdraw(400)
print(b.getBalance())
In the example above, we could write something like this outside the class: b.__balance = 1
, however it would create a new class attribute and not access the private one.
Static objects
A static variable is a variable that retains its value between method calls. Its memory address is allocated once, and it exists for the duration of the program. A variable of this type will still exist even if an instance of the class does not. It is shared by all objects and not tied to any class instance (shared across all instances of the class). We create static variables outside methods. A static variable is also called a class attribute. I will cover static methods in a lesson about decorators.
class Example:
static_variable = 0
def method(self):
Example.static_variable += 1
x = Example()
x.method()
x2 = Example()
x2.method()
print(x.static_variable, x2.static_variable, Example.static_variable)
Example.static_variable2 = 3 # we can also create class attributes outside the class
print(Example.static_variable2)
Method errors
There are two ways of safely checking whether a class contains a particular method.
class Example:
def method(self):
print("x")
x = Example()
if hasattr(x, "method"):
x.method()
else:
print("Error")
class Example:
def method(self):
print("x")
x = Example()
try:
x.method()
except AttributeError:
print("Error")
Serialization
Serialization means converting the state of an object (its data) into a format that can be easily stored and reused. We can serialize an instance of a class using the pickle
module (the file will have the .objc
extension). We can later import the object from the file in another program.
import pickle as pick
class Example:
def method(self):
print("x")
e = Example()
try:
with open("file.objc", "wb") as file:
pick.dump(e, file)
except:
print("Error")
with open("file.objc", "rb") as file:
example = pick.load(file)
example.method()