Skip to content

3 Object Oriented Programming

Dimitris Tsapetis edited this page Nov 12, 2022 · 3 revisions

For a video presentation of the following content, please follow this link

Objects/Classes

Object-Oriented Programming (OOP) in Python 3 - Real Python

Object-oriented programming - Object-Oriented Programming in Python 1 documentation

Primitive data types such as int, bool and dict in Python are simple ways in which data are represented. As the code becomes more complex, these simple data types are not sufficient to express the information at hand. For example, in order to describe a Person as a data type, multiple data of simpler types need to be bundled together. These data can be strings such as the FirstName and LastName of the Person or integers such as the Age.

To create an object, object oriented programming languages define “blueprints” that are used as a guidelines to create these new user-defined data structures. This “blueprint” is called a class and bundles together the data as properties. The functionalities that act on these data, are called methods.

🚨 ***Classes Vs Instances***

Imagine classes as “blueprints” of a building floor. Its properties might include the number of doors, or the dimensions of each room. Its functions can include actions operated, such as open door/window or tidy up room etc.

https://home3ds.com/advantages-converting-2d-floor-plan-3d-floor-plan/

The class or “ floor blueprints” in our case, provide information to the contractor on how the floor should look like after construction ends. The constructed floor illustrated is the actual object or instance.

Classes can be used to create objects, the same way that a blueprint can be used to create floors that look alike. One class can be used to create multiple instances of the same user-defined data type.

How to define a class?

All classes are defined by the keyword class followed by the class name and a colon :. The code indented below after the definition, belongs to the class body. In the case shown below, the pass keyword is used. This allows to create an empty class without any implementation.

class Person:
	pass
⚠️ ***Class naming convention:*** According to Python’s naming conventions (PEP-8), classes names should be written using `PascalCase`. In other words, capitalize the first letter of each word. An example in the UQ field would be `PolynomialChaosExpansion`.

Naming conventions are not just an another directive. They are not mandatory rules, but a consensus between developers of the same language, in order to point out the different uses of parts of the code. ESPECIALLY when sharing code, these conventions allow the community to understand it faster. But even when writing code for internal use, these conventions enhance readability and make code clear and comprehensible.


More on the PEP-8 conventions can be found here:

PEP 8 - Style Guide for Python Code

Constructor/Initializer

Python Class Constructors: Control Your Object Instantiation - Real Python

The creation of a new object in Python is split into two discrete steps. First, the construction phase of the object, followed by the initialization phase.

  • Constructor (__new__)

    def __new__(cls, *args, **kwargs):
    	return super().__new__(cls)

    This first step is part of the construction process in Python. The special method __new__ is responsible for creating and returning a new empty object, along with the class initializer arguments.

    ⚠️ In most cases the developer does not have interaction with the `__new__` method and Python takes care of its execution.
  • Initializer (__init__)

    def __init__(self):
    	pass

    The __init__ special method is the one used for initializing a new object. As the name implies, the purpose of this method is to define the initial state of the object. Note that the first parameter, self is the instance of the object already created and inside the initializer new attributes can be defined.

    💣 The initializer has no return. If the developer returns a value, then a `TypeError` will be thrown.

Attributes

Python supports two types of attributes that can belong to a class, Instance and Class attributes.

Instance Attributes

Instance attributes are object properties that share the same name, differ in contents between objects. An example on how to define an instance attribute is displayed below:

class Person:

	def __init__(self, first_name, last_name, age):
		# Instance Attributes
		self.first_name=first_name
		self.last_name=last_name
		self.age=age

Class Attributes

In contrast to instance variables, the values of class attributes is shared by all instances of the same class. They can be used to define a constant or a share property. In the case of the Person class, the class attribute can be for example the species all persons belong to. The definition of a class attribute is right before the __init__ method as shown below:

class Person:
	#Class Attribute
	species = "Homo Sapiens"

	def __init__(self, first_name, last_name, age):
		# Instance Attributes
		self.first_name=first_name
		self.last_name=last_name
		self.age=age
⚠️ ***Attribute naming conventions:*** All variables and attributes in Python are named using the `snake_case` convention, where all letters are lowercase and words are separated by underscores. In case we need to define a private variable, even though access modifiers are not available, the leading underscore allows us to warn other user about that case (e.g. `_private_variable`).

Naming conventions are not just an another directive. They are not mandatory rules, but a consensus between developers of the same language, in order to point out the different uses of parts of the code. ESPECIALLY when sharing code, these conventions allow the community to understand it faster. But even when writing code for internal use, these conventions enhance readability and make code clear and comprehensible.


More on the PEP-8 conventions can be found here:

PEP 8 - Style Guide for Python Code

Property

Python's property(): Add Managed Attributes to Your Classes - Real Python

Properties are an extension of attributes and are often called managed attributes. It is a python functionality that allows developers to avoid getter and setter methods for class attributes.

class Person:

	def __init__(self, first_name, last_name, age):
		# Instance Attributes
		self.first_name=first_name
		self.last_name=last_name
		self.__age=age 
	
	@property
	def age(self):
		"Get the age property"
		return self.__age

	@age.setter
	def age(self, value):
		self.__age=value

Methods

Instance methods

Instance methods, are functionalities defined inside a class. Since this functionalities usually depends on the instance attributes of a class, it can only be called using an object. The first parameter of these methods is always self. An example of such an instance method is displayed below.

class Person:
	#Class Attribute
	species = "Homo Sapiens"

	def __init__(self, first_name, last_name, age):
		# Instance Attributes
		self.first_name=first_name
		self.last_name=last_name
		self.age=age

	def introduce_yourself(self):
		print(f"My name is {self.first_name} {self.last_name}!")

Static methods

Static methods are a special type of methods. Contextually, they are related with the object but do not use the object data at all. They are usually auxiliary methods and require no object instance to execute their functionality.

class Person:
	#Class Attribute
	species = "Homo Sapiens"

	def __init__(self, first_name, last_name, age):
		# Instance Attributes
		self.first_name=first_name
		self.last_name=last_name
		self.age=age

	def introduce_yourself(self):
		print(f"My name is {self.first_name} {self.last_name}!")
	
	@staticmethod
	def clean_room(room_name):
		print(f"I am cleaning the {room_name}")

OOP Principles

Polymorphism, Encapsulation, Data Abstraction and Inheritance in Object-Oriented Programming | nerd.vision

Inheritance

Types of inheritance Python - GeeksforGeeks

Inheritance is the capability of OOP languages to inherit attributes and method of other classes. This allows developers to maintain the same interface of a class, while allowing to change the internal implementation. The existing classes are called base, super or parent. The deriving classes on the other hand are called, sub- or child classes.

Single Inheritance

The simplest form of inheritance is single inheritance. In this case, a subclass inherits all attributes and methods of the base class. This enables code reusability, while allowing the addition of new features.

Below, an example of extending the Person class is provided. Existing attributes are initialized using the super class __init__ method. While existing new ones are saved within the child class __init__ method.

As far as methods are concerned, the methods of the parent class can be used as is. If modifications to the functionality are needed, the methods can be overriden, to completely change or partially add functionality to the method of the parent class.

class Employee(Person):

	def __init__(self, first_name, last_name, age, job_title):
		super().__init__(first_name, last_name, age)
		self.job_title=job_title

#	def introduce_yourself(self):
#		  print(f"My name is {self.first_name} {self.last_name} and I work as a {self.job_title}!")

# def introduce_yourself(self):
# 		super().introduce_yourself()
# 		print(f"I work as a {job_title}")

Multilevel Inheritance

A second more advanced type of inheritance is Multilevel Inheritance. Here there is a sequence of subclasses that serve as parents for new deriving classes. The final subclass, inherits all attributes and functionality of all, previous parent classes. An example of this inheritance type is shown below:

The implementation of the later inheritance in Python code is the following:

class PostDoc(Employee):

	def do_research():
		are_results_novel = False
		while not are_results_novel:
			eat()
			sleep()
			research()

	def write_paper():
		print("writing.....")
		print("writing.....")
		print("writing.....")
		print("writing.....")
		print("writing.....")

	def code():
		print("Debugging UQpy v1038274")

Polymorphism

Polymorphism in Python - GeeksforGeeks

Polymorphism in Python | Object Oriented Programming (OOPs) | Edureka

The concept of Polymorphism is object-oriented programming, is the supply of a single signature that can treat multiple types of data.

The first and simplest method of polymorphism is ad-hoc polymorphism. In dynamically typed languages like Python, this type of polymorphism is achieved by inspecting the argument type and performing different operations accordingly. One example if the in-build len() function shown below:

Polymorphism in built-in function len() (Ad-hoc)

students = ['Student1', 'Student2', 'Student3']
name='Dimitris'
len(students)
len(name)

Polymorphism with inheritance

In Python, inheritance allows to define child classes that share the same methods and interface as the parent class. Inherited method can be overriden, while maintaining the same interface. As a result, we have access to methods of same signature but different implementations.

  • Method overriding

    class Employee(Person):
    
    	def __init__(self, first_name, last_name, age, position):
    		super().__init__(first_name, last_name, age)
    		self.position=position
    
    #	def introduce_yourself(self):
    #		  print(f"My name is {self.first_name} {self.last_name} and I work as a {self.job_title}!")
  • Method overloading

    💣 Method overloading NOT supported, how to do it instead
    class Rectangle:
        # function with two default parameters
        def area(self, a):
    	    print('Area of Square is:', a ** 2)
    
    		def area(self, a, b):
    			print('Area of Rectangle is:', a * b)
    class Shape:
        # function with two default parameters
        def area(self, a, b=0):
            if b > 0:
                print('Area of Rectangle is:', a * b)
            else:
                print('Area of Square is:', a ** 2)

Abstraction

Abstract Base Class (abc) in Python - GeeksforGeeks

https://www.mygreatlearning.com/blog/abstraction-in-python/#:~:text=What is Abstraction in Python,oriented programming (OOP) languages.

Abstraction in Python is the process of hiding internal functionality from the user. The goal of this process is to reduce the complexity and allow the generalization of concepts and code architecture. As a result, the user now is familiar with the type of work performed by a function, but not its specific implementation. Abstraction in Python is achieved with the aid of abstract classes ABC and abstractmethod.

Abstract Classes

Abstract base classes are a way enforce a common interface between objects. In way they are a “blueprint” for creating new classes. They define the signature of methods that deriving classes must implement and enforce it. Since ABC cannot be instantiated, child classes must be defined. At the same time their child classes are guaranteed to adhere to the predefined abstract class specifications.

from abc import ABC

class Shape(ABC):
	pass

Abstract Methods

Even though abstract classes can have concrete implementations of some of its methods, their main goal is to define the methods that need child class specific implementations. This is achieved by using the abstractmethod decorator. This prevents the instantiation of any subclasses that do not override the specific method with a custom implementation. Otherwise a TypeError is raised.

from abc import ABC,abstractmethod

class Shape(ABC):
	@abstractmethod
	def calculate_area():
		raise NotImplementedError("Subclasses should implement this!")

Encapsulation

Encapsulation is one of the fundamental concepts of OOP. It describes the idea of bundling data and functionalities in units such as Python classes. This idea is usually combined with information hiding, which is the concept of keeping the internal data of a class hidden from its the outside world. This prevents accidental access and modification of private class data. In most programming languages, access modifiers are used for data hiding while getter and setter methods retrieve and modify the value in a controlled way respectively.

Access Modifiers

Python does NOT share the same access modifiers like public, private and protected available elsewhere. Instead a similar behavior is achieved by using variable naming conventions and using single (_) and double (__) leading underscore for class attributes.

  • Public access convention
self.name #Public attribute
  • Protected access convention

The goal of attribute names prepended with a single underscore is to define protected members. These members of a class are called protected and are visible to child classes. However they are publicly accessible in Python. This naming convention existing to deter programmer from using these attributes outside the class.

self._name #Protected attribute
  • Private access convention

In case of instance attributes with names prepends with a double underscore is to provide private variables and thus restrict access to them. If code external to the class tries to access the attribute then an AttributeError is raised. During runtime python performs an operation named name mangling with prepends an underscore and the class name to the variable thus making it not directly accessible.

self.__name #Private attribute

Getters & Setters

Descriptor HowTo Guide - Python 3.10.8 documentation

One common way of handling access and modification of data is with the use of getter and setter methods. This allow the class to hide its data from the public, while allowing setting their new values under controlled conditions. On example of getter and setter methods is displayed below.

class Employee(Person):

	def __init__(self, first_name, last_name, age, position, salary):
		super().__init__(first_name, last_name, age)
		self.__position=position
		self.__salary=salary

	# I can tell you my current position, but you cannot change it
	def get_position(self):
		return self.__position 

	# I will not share my salary info, but you can give me a pay raise
	def set_salary(self, salary):
		if salary >= self.__salary:
			self.__salary=salary
		else:
			raise ValueError("You can only give me a pay raise!")

These methods can also be replaced with the simpler properties available in Python.

class Employee(Person):

	def __init__(self, first_name, last_name, age, position, salary):
		super().__init__(first_name, last_name, age)
		self.__position=position
		self.__salary=salary
	
	@property
	def position(self):
		return self.__position
	
	def salary(self, new_salary):
		if new_salary >= self.__salary:
			self.__salary=new_salary
		else:
			raise ValueError("You can only give me a pay raise!")

	salary = property(None, salary)