Sławomir Kwiatkowski

by: Sławomir Kwiatkowski

2024/06/29

Basics of object-oriented programming in Python

  1. How to create objects in Python
  2. Inheritance and multi-inheritance
  3. Extending method code
  4. Overriding method code
  5. Init() method
  6. Class variables
  7. Class methods, constructor overloading
  8. Static methods

 

Let's take a class called Person as an example. By convention, a class name should start with a capital letter, and in the case of a name consisting of many words, we use Pascal formatting - all words written together and starting with a capital letter. The basic class definition is as follows:

     
class Person:
    pass

All classes inherit from the base object class by default, so you could define the class as follows:
     
class Person(object):
    pass

Objects can be created from a class that contains only an empty pass statement. Creating an instance of an object does not require a special keyword, such as new
     
john_doe = Person()   # creates an instance of Person class

print(john_doe)   # shows the instance class name and the instance address in memory
					e.g. _main__.Person object at 0x7f5d0993a780
                    
print(dir(john_doe))   # shows all available class methods
 
To define a class that inherits from another class, provide the name of the base class as an argument.
     
class Employee(Person):
    pass

Python supports multi-inheritance. You can create a class that inherits from multiple base classes.
     
class Executive(Employee, Manager):
    pass 

When a child class inherits from several base classes, it also inherits the attributes and methods from those classes.
However, when there are attributes or methods with the same names in two different base classes, the child class inherits this attribute or method whose class was first on the argument list in the child class definition.
     
class Person:
    name = 'Person class'
    def func_1(self):
        return "Func_1 from Person class"

class Employee:
    name = "Employee class"
    def func_1(self):
        return "Func_1 from Employee class"
    def func_2(self):
        return "Func_2 from Employee class"

class Manager(Person, Employee):
    pass
    
    
if __name__ == '__main__':
    john_doe = Manager()
    print(john_doe.name)       # returns text: Person class
    print(john_doe.func_1())   # returns text: Func_1 from Person class
    print(john_doe.func_2())   # returns text: Func_2 from Employee class

When inheriting a method from a base class, child class extends the functionality of the method. The child class's init() method has additional functionality:
     
class Person:
    def __init__(self, name):
    	self.name = name
        
class Employee(Person):
    def __init__(self, name, job):
    	super().__init__(name)
    	self.job = job
        
if __name__ == '__main__':
    john_doe = Employee('John Doe', 'programmer')
    print(john_doe.name, john_doe.job)      #returns: John Doe programmer

You can use a parent class name instead of function super(). This is especially useful when a child class inherits from several base classes and we want to use a specific method from a specific base class.
     
class Employee(Person):
    def __init__(self, name, job):
    	Person.__init__(self, name) 
    	self.job = job

You can overwrite a method and then get completely new functionality:
     
class Person:
    def __init__(self, name):
    	self.name = name
    def greeting(self):
    	return 'Hello'

class Employee(Person):
    def __init__(self, name, job):
    	super().__init__(name)
    	self.job = job
    def greeting(self):
    	return "Hi!"

if __name__ == '__main__':
    john_doe = Employee('John Doe', 'programmer')
    print(john_doe.greeting())      #returns: Hi!

When an instance of a class is created, a special init() method is executed, which can be compared to a constructor from other languages. The first argument is self by default, which is a reference to the current class. The remaining arguments are optional:
     
class Person:
    def __init__(self, *args):
    	for arg in args:
            print(arg, end=' ')

john_doe = Person("John", "Doe", "programmer") #returns: John Doe programmer

The init method, like constructors from other languages, is used to set attribute values. Instance attribute names must be preceded by the self parameter:
     
class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary
        
        
john_doe = Employee('John Doe', 8000)
print(john_doe.name)    # returns: John Doe
print(john_doe.salary)	# returns: 8000

In addition to instance variables, there are also class variables. They hold values that are the same for all instances of the class.
It is also possible to display the values of these variables without creating an instance of the class, just by specifying the class name:
     
class Employee:
    bonus = 10
       
john_doe = Employee()   
print(john_doe.bonus)   # returns: 10
print(Employee.bonus)   # returns: 10

A class variable can store, for example, the number of all employees in a company. It has the same value regardless of whether it is called from the class or from its instance. The increment of the class variable storing the number of employees can be placed, for example, in the init() method, then creating a new employee increases the value of the class variable.
However, you can create an instance variable with the same name as the class variable. In this case, the class variable will be overridden by the value of the attribute:
     
class Employee:
    bonus = 10
    def set_bonus(self, bonus):
        self.bonus = bonus

john_doe = Employee()
print(john_doe.bonus)   # returns:  10
john_doe.set_bonus(20)  # sets a bonus to john_doe
print(john_doe.bonus)   # returns: 20
print(Employee.bonus)   # returns 10 - base bonus  

To create a class method, use the cls argument instead of self, which is a reference to the class, not the instance. Additionally, you must use the @classmethod decorator:
     
class Employee:

    bonus = 10

    @classmethod
    def set_base_bonus(cls, bonus):
        cls.bonus = bonus


print(Employee.bonus)		#returns: 10
Employee.set_base_bonus(20)
print(Employee.bonus)		#returns: 20

The above method sets the base value of the variable bonus in the Employee class and all its instances.
Class methods can also be a way to create alternative constructors with a different number of arguments (constructor overloading).
For example, considering the example Employee class, which takes the employee's name and the salary value as arguments, we can create a class method that takes 3 arguments: first name, last name and salary value and returns an instance of the Employee class.
     
class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary
    @classmethod
    def from_full_name(cls, first, last, salary):
        name = '{} {}'.format(first, last)
        return cls(name, salary)

john_doe = Employee.from_full_name('John', 'Doe', 8000)

print(john_doe.name, john_doe.salary)	#returns: John Doe 8000

There are static methods preceded by the @staticmethod decorator. Static methods do not take a reference to a self-instance or a cls value to a class as an argument. They can accept explicit arguments as parameters of these functions. Static functions are thematically related to the class in which they are located.
     
class Employee:
    
    @staticmethod
    def deadlines(project):
        projects = {
            "First": "2024-06-30",
            "Second": "2024-07-10"
        }
        deadline = projects.get(project, "No data")
        return {'Project': project, 'Deadline': deadline}


print(Employee.deadlines("First"))      #returns: {'Project': 'First', 'Deadline': '2024-06-30'}

print(Employee.deadlines("Second"))     #returns: {'Project': 'Second', 'Deadline': '2024-07-10'}

print(Employee.deadlines("Third"))      #returns {'Project': 'Third', 'Deadline': 'No data'}