# Python Class constructor, destructor, accessibility, and inheritance

## Define a function in a class

Functions in a class can be defined in the following way. Once it’s defined, it needs to be instantiated.

class FirstClass():
def return_hello(self):
return "Hello"

instance = FirstClass()
print(instance.return_hello())
# Hello

In many other languages, new keyword is needed to instantiate a class but it’s not necessary in Python.

But what’s self in a function? I defined the function without it but it didn’t work.

class FirstClass():
def return_hello():
return "Hello"

instance = FirstClass()
print(instance.return_hello())
# Traceback (most recent call last):
#   File ".\src\class.py", line 9, in <module>
#     print(instance.return_hello())
# TypeError: return_hello() takes 0 positional arguments but 1 was given

The first argument must always be self and we can’t omit it from the definition.

Hmm… it’s not good. Why do we always need to write it…

But anyway, it’s needed.

## How to define constructor

If some initializations are needed for the class, it can be defined in __init__ function. This is the constructor in Python.

The first argument is always self, and then, we can define additional variables if necessary. Let’s assume that we want to receive name value from outside.

class FirstClass():
def __init__(self, name):
self.name = name

def return_hello(self):
return f"Hello {self.name}"

instance = FirstClass("Yuto")
print(instance.return_hello())
# Hello Yuto

To use the value in the whole class, the value needs to be assigned to self.variable_name.

## How to define destructor

In opposite to the constructor, the class might need to do resource release or something else when the instance is not used anymore. In this case, it can be defined in __del__ function.

class FirstClass():
_protected_one = 1
__private_one = 1

def __init__(self, name):
self.name = name

def __del__(self):
print("Destructor for FirstClass was called")
self.name = None
print(f"name was set to {self.name}")

instance = FirstClass("Yuto")
print(instance.name) # Yuto
exit()
# Destructor for FirstClass was called
# name was set to None

The instance is no longer referred to when calling exit function. Then, the destructor is called.

## Private or Public accessibility

There is no keyword like private, public, protected or similar stuff. We need to indicate them by the variable name.

Let’s try to access them.

class FirstClass():
_protected_one = 1
__private_one = 1

def __init__(self, name):
self.name = name

def return_hello(self):
return f"Hello {self.name}"

def use_members(self):
print("protected: " + str(self._protected_one))
print("private: " + str(self.__private_one))

instance = FirstClass("Yuto")
print(instance.return_hello()) # Hello Yuto
print(instance._protected_one) # 1
print(instance.use_members())
# protected: 1
# private: 1
print(instance.__private_one)
# None
# Traceback (most recent call last):
#   File "D:\DMGMORI_Development\private\blogpost-python\src\class.py", line 28, in <module>
#     print(instance.__private_one)
# AttributeError: 'FirstClass' object has no attribute '__private_one'

Both protected and private variables can be accessed in the class but it throws an error when accessing the private variable via the instance.
Note that the protected variable can be accessed from the instance but it is not recommended.

The private/protected accessibility is applied to functions too.

## Inheritance

### Create a class based on another class

There are many cases where a class needs to be created based on another class. It is called Inheritance.

When a class needs to inherit another class, the base class must be specified in the argument of the class.

class FirstClass():
def __init__(self, name):
self.name = name

def __del__(self):
print("Destructor for FirstClass was called")
self.name = None
print(f"name was set to {self.name}")

def return_hello(self):
return f"Hello {self.name}"

def do_something(self):
raise NotImplementedError("Method not implemented")

class SecondClass(FirstClass): # A inherited class must be specified here
def __init__(self, name, age):
self.age = age
super().__init__(name)

def __del__(self):
print("Destructor for SecondClass was called")
self.age = None
print(f"age was set to {self.age}")
super().__del__()

def return_hey(self):
return f"Hey {self.name} {self.age}"

If SecondClass is instantiated, return_hey function can be called as well as return_hello function that is defined in FirstClass.

instance2 = SecondClass("Yuto2", 35)
print(instance2.return_hello()) # Hello Yuto2
print(instance2.return_hey())   # Hey Yuto2 35

Let’s call do_something function here.

try:
print("Call do_something()")
instance2.do_something()
except BaseException as e:
print(f"ERROR: {e}")

# Call do_something()
# ERROR: Method not implemented

do_something function is defined but it just raises an error in FirstClass That’s why an error occurs here.

### Use inheritance as an interface definition

If a function in the base class raises an error, it must be implemented in the inherited class. It means that the class can be used as an interface.

class Person():
def __init__(self, name, age):
self._name = name
self._age = age

def work(self):
raise NotImplementedError("Method not implemented")

def greet(self):
raise NotImplementedError("Method not implemented")

Then, let’s define the following two classes based on Person class.

class Developer(Person):
def work(self):
print("I'm developing something...")

def greet(self):
print(f"Hi, I'm a developer. Name: {self._name}, Age: {self._age}")

class Manager(Person):
def work(self):
print("I'm creating a plan...")

def greet(self):
print(f"Hi, I'm a manager. Name: {self._name}, Age: {self._age}")

If we provide the type info, IntelliSense can understand which function is available on the instance. As you can see in the following, the function result depends on the instance.

from typing import List
persons: List[Person] = [Developer("Yuto", 35), Manager("TOTO", 55)]
for instance in persons:
instance.greet()
instance.work()
# Hi, I'm a developer. Name: Yuto, Age: 35
# I'm developing something...
# Hi, I'm a manager. Name: TOTO, Age: 55
# I'm creating a plan...

### Use ABC module for abstract class

The interface-like definition can be done in a previous way but using ABC module is better.

from abc import ABC, abstractmethod

class Person(ABC):
def __init__(self, name, age):
self._name = name
self._age = age

@abstractmethod
def work(self):
pass

@abstractmethod
def greet(self):
pass

The base class can be instantiated in a previous way but not in this way. If we try to instantiate it, we get the following error.

Traceback (most recent call last):
File "~\src\class.py", line 139, in <module>
abstract_class2()
File "~\src\class.py", line 130, in abstract_class2
Person("Yuto", 35)
TypeError: Can't instantiate abstract class Person with abstract methods greet, work

### Multi inheritance

Let’s check how it works if a class has multi-base classes.

class A():
def hello(self):
return "hello from A"

def boo(self):
return "boo"

class B():
def hello(self):
return "hello from B"

def beeee(self):
return "beeee"

class C(A, B):
pass

instance = C()
print(instance.hello()) # hello from A
print(instance.boo())   # boo
print(instance.beeee()) # beeee

The first base class definition is used if the same function is defined in the different base classes. The non-conflicted function can of course be used.

But this is not a good design. Don’t do this.

Python