#!/usr/bin/env python # coding: utf-8 # # Procedural, Functional, and Objected-Oriented Programming Paradigms # ## TABLE OF CONTENTS # 1. [Introduction](#introduction)
# 2. [Overview of Programming Paradigms](#overview_of_pp)
# 2.1 [Procedural](#procedural)
# 2.2 [Functional](#functional)
# 2.3 [Object-Oriented](#object_oriented)
# 3. [Example Case: Building a Car](#example_case)
# 3.1 [Specifying the Model](#specifying_the_model)
# 3.2 [Defining the Attributes](#defining_the_attributes)
# 3.3 [Defining the Functionality](#defining_the_functionality)
# 3.4 [Building a Different Car](#building_a_different_car)
# 4. [Four Fundamental Principles of Object-oriented Programming](#FunctionalFunctionalfundamental_principles") # 5. [Sources](#sources) # ## Introduction # # Just like many other programming languages out there, Python's functionality builds on so-called __programming paradigms__. While some languages may require you to use a certain paradigm, others allow for more than one paradigm in your code. # # Python is mostly referred to as an __object-oriented__ programming language, implying that only Object-Oriented Programming (OOP) - one of many paradigms - is allowed in it. However, the language supports and encourages the use of other paradigms too. Users can mix and match the paradigms in a way that suits the problem at hand and matches their programming style. This feature grants the user a lot of flexibility and is probably one reason why Python has become so popular. It would, therefore, be more accurate to speak of Python as a multi-paradigm programming language instead of a purely object-oriented one. # # In the following sections, we will first provide you with a brief overview of the main paradigms in Python. Then, by building Tesla's Model X step by step, we will be illustrating the differences between the paradigms and show that each of them would do the trick. # # # ## Overview of Programming Paradigms # # Now, what exactly are programming paradigms? Roughly speaking, a paradigm is a way of solving a problem. Each paradigm offers a different set of ideas to perform the task at hand. They follow a paradigm-specific approach, structure your code and control its execution. # # The three main paradigms that are supported by Python are: # 1. [Procedural](#procedural)
# 2. [Functional](#functional)
# 3. [Object-Oriented](#object_oriented)
# # They differ mainly in when and how the program assigns values to variables. Controlling this is important because it directly affects the manageability and reliability of the code. Chaotic, repetitive or copy-paste codes are more prone to mistakes, difficult to read and hard to maintain. Programming paradigms assist you in writing well-structured code so that it remains comprehensible and concise. # # ### Procedural # # The __procedural__ paradigm might be the most intuitive of the three. It uses a top-down approach, where it breaks the problem down into small pieces and instructs the machine step by step how to solve a task. The state of a variable is changed directly along the way, which makes the code very easy to follow and understand. For the machine it is like following a recipe. # # However, the longer and more complex a code, the more difficult it gets to stay on top of things. For example, a programmer would always have to know which variable names he already used because accidentally assigning a value to an existing variable may cause problems down the line. Finally, reusing a program that follows the procedural paradigm is often not that easy and requires you to code up every step again. # # ### Functional # # While the procedural paradigm follows a step by step approach, the __functional__ paradigm solves a task by executing a series of functions. In this case, the state of a variable is changed by applying a function to it. The definition of the function happens strictly seperate from its use and is done in a general way, such that we can use it again with different inputs. This procedure might be a little more time consuming at first, but it has the advantage that later on, we can use the function for similar tasks without having to code every instruction again. # # This reusability makes the code a lot shorter. However, one will have to look up the function definition to understand what it does and how it changes the state of the variable. Additionally, when working with imported functions there is always the possibility of unwanted changes to their functionality. If the package creators decide to change said function, this will affect your program too. # # # # ### Object Oriented # # The __object-oriented__ programming paradigm is built on the notion of classes and objects. These objects are similar to real life objects and all objects of the same class share certain attributes (states) or methods (functionalities). However, each object can also have personal attributes, just like one jacket might be red and the other one might be green, they both have the ability to keep you warm though. # # As you see, with OOP state and function are no longer separated but happen both within the object. The problem at hand is then solved through the interaction between the different objects. For example, if you, as a human object, are cold you could put on the jacket object to solve that problem. The fact, that OOP basically mimicks the real world, makes it easier to understand and since these objects are whole in themselves, they can easily be reused in different applications. # # # ## Example Case: Building a Car # In the following sub-chapters, the three different paradigms - procedural, functional and object oriented - will all be illustrated by the same example: the construction of the Model X car by the manufacturer Tesla. The aim of this chapter is to convey to the reader that the three paradigms are not completely different 'things', but rather allow a different perspective or approach to tackle one and the same problem. The example with the car, a simple everyday object, is used to convey the concepts of the three paradigms more easily. To get a better idea of the car, here is a picture of the original Model X by Tesla: # # ![Markdown](imgs/06_model_x.png) # # The steps in the construction of the car are as follows: # # 1. [Specifying the Model](#specifying_the_model)
# 2. [Defining the Attributes](#defining_the_attributes)
# 3. [Defining the Functionality](#defining_the_functionality)
# # Note, if a string is printed in the following subchapters (e.g. "Model X is driving"), the implied action is considered to take place immediately. # ### 1. Specifying the Model # In this chapter, the exact model to be built is specified. # In principle, it would also be possible to build another model instead of the Model X (for example, the Model S). # #### Procedural # In the procedural way, the car model is specified simply through assigning the name of the model to a variable of choice: # In[1]: # specifying the model model_x = "Model X" # show the specified model print("The model under construction is: {}".format(model_x)) # #### Functional # In the case of the functional paradigm, we create a function called ````specify_model```` and pass the desired name of the model to the function through the parameter called ````model_name````. In the suite of the function the model is specified locally by assigning the passed name of the model to the local variable called ````model````. In this context, local means the variable is only usable inside the suite of the function (the indented lines below the signature/name of the function). In the last step of the function, the value of the variable ````model```` is returned and assigned to the global variable called ````model````. This last step has to be done in order to make the specified model globally available. # In[2]: # function to specify the model def specify_model(model_name): # specifying the model locally model = model_name # show the specified model print("The model under construction is: {}".format(model)) # return the specified model return model # specifying the model globally model_x = specify_model("Model X") # #### Object Oriented # And lastly, turning our viewpoint to the object oriented paradigm: # # A class is like a blueprint for an object. Later in this tutorial you will see that a class specifies all the details of an object derived from that class. In the code below, the ````class```` keyword is used to define an empty class ````Car````. Any indented code that follows the colon after the class definition represents the body of the class. For now, the ````pass```` statement is used as a placeholder to indicate where code in later steps will eventually go. It allows you to run the code without any errors. # In[3]: # class definition class Car: # body of the class pass # Creating a new object from a class is called "instantiating an object". You can instantiate a new object of the class ````Car```` by assigning the name of the class, followed by opening and closing parentheses, to a variable of choice: In this case the new object of the class ````Car```` is instantiated and assigned to the variable ````model_x````. You can create as many objects of a class as you wish and assign each of them to a different variable. Altough they will all be instances of the same class, they represent independent objects, which may have different details. # In[4]: # instantiating the object model_x = Car() # show the specified model print("The model under construction is: {}".format(model_x)) # If you execute the code above, you may wonder why strange characters are displayed instead of the name of the model like in the paradigms shown before. This is due to the fact that we have not yet specified any attributes in the class ````Car````. Therefore, the instantiated objects of this class also have no attributes. This will be rectified in the next chapter of this example. The character string displayed corresponds to the address of the instantiated object in the main memory. # So far, we have only specified which model we want to build. # It's not really a car yet, because it doesn't have any physical attributes like colour or basic functionality like driving. # # # ### 2. Defining the Attributes # # This chapter focuses on the individual characteristics that differentiate a car (Model X) from another by determining its appearance, condition or other qualities. # #### Procedural # # In the same way as in specifying the model, the attributes are specified simply by assigning the individual characteristics to a variable of choice: # In[5]: # specifying the model model_x_name = "Model X" # specifying the attributes model_x_colors = ["white", "black", "silver", "blue", "red"] model_x_hp = 443 model_x_length = 5.04 model_x_width = 2.27 # printing the model and the attributes print("The {} has the following specifications:".format(model_x_name)) print("Available colours: {}".format(model_x_colors)) print("HP: {}".format(model_x_hp)) print("Length: {}".format(model_x_length)) print("Width: {}".format(model_x_width)) # #### Functional # # In functional paradigm, the individual characteristics of our Model X are specified by the function `car_attr`. # Firstly, we define the parameters of the newly created function `car_attr`. The parameters form the different car's attributes, in our case: name, colors, hp, length, width. # In[6]: # function to specify the model def specify_model(model_name): model = model_name print("The model under construction is: {}".format(model)) return model # function to specify the attributes def car_attr(name, colors, hp, length, width): """ Prints the specifications. Returns a list containing the car's specifications. """ car_attr = [name, colors, hp, length, width] print("The {} has the following specifications:".format(name)) print("Available colours: {}".format(colors)) print("HP: {}".format(hp)) print("Length: {}".format(length)) print("Width: {}".format(width)) return car_attr # Secondly, once the function is created, we can use it by assigning the specific characteristics to our Model X. # In[7]: # specifying the model model_x = specify_model("Model X") # specifying the attributes model_x_attr = car_attr(name = model_x, colors = ["white", "black", "silver", "blue", "red"], hp = 443, length = 5.04, width = 2.27) # #### Object Oriented # A class does not actually contain any data. A method called `__init__()` specifies that different parameters are necessary to define a certain car, but it does not contain the actual values for these parameters for any specific car. Every time a new car object is created, `__init()__` sets the initial state of the object by assigning the values of the object’s properties. That is, `__init()__` initializes each new instance of the class. # # You can create as many attributes in the `__init()__` method as you like, but the first parameter will always be a variable called self. When a new class instance is created, the instance is automatically passed to the self parameter in `__init()__` so that new attributes can be defined for the object. Note in the code below, the `__init()__` function is indented by four spaces and the body of the method by eight spaces. This exact indentation is important to Python, as it indicates that the `__init()__` method belongs to the Car class. # # In the body of `__init()__`, there are four statements - one for each attribute - using the self variable: # # 1. __self.colors = colors__ creates an attribute called colors and assigns to it the value of the colors parameter. # 2. __self.hp = hp__ creates an attribute called hp and assigns to it the value of the hp parameter. # 3. __self.length = length__ creates an attribute called length and assigns to it the value of the length parameter. # 4. __self.width = width__ creates an attribute called width and assigns to it the value of the width parameter. # # As mentioned above, all attributes created in the `__init()__` method are specific to the instances and hence referred to as instance attributes. In other words, all car objects have a length, width, etc. but the values for these parameters vary depending on the Car instance. # # In order to create attributes that all objects inherit, so called class attributes, a value can be assigned to a variable name outside the `__init()__` function. Exemplary, all Cars of our class are models by Tesla. # In[8]: class Car: #class atrributes brand = "Tesla" #instance attributes def __init__(self, name, colors, hp, length, width): self.name = name self.colors = colors self.hp = hp self.length = length self.width = width # In[9]: # instantiating the object model_x = Car(name = "Model X", colors = ["white", "black", "silver", "blue", "red"], hp = 443, length = 5.04, width = 2.27) # printing the name and class attribute print("The {} is a car by {}.".format(model_x.name, model_x.brand)) # printing some instance attributes print("The {} has a length of {}m and a width of {}m.".format(model_x.name, model_x.length, model_x.width)) # Our Model X is starting to have a physical appearance thanks to the individual characteristics assigned to it such as its color or width. However, our car still lacks some functionalities to be usable (such as driving). # # # ### 3. Defining the Functionality # # In order to finalize the car, specific methods that define the car's functionality have to be implemented - exemplary, in our case the driving function. # #### Procedural # In the procedural way, the car's driving function is executed through a simple print statement. # In[10]: # specifying the model model_x = "Model X" # specifying the attributes model_x_colors = ["white", "black", "silver", "blue", "red"] model_x_hp = 443 model_x_length = 5.04 model_x_width = 2.27 # specifying the functionality mph = 100 print("{} is driving at {} mph.".format(model_x, mph)) # #### Functional # For the functional paradigm, the function `drive` is implemented in the same way that the car's attributes are defined. Within the `drive` function, a simple print statement is used. # In[11]: # function to specify the model def specify_model(model_name): model = model_name print("The model under construction is: {}".format(model)) return model # function to specify the attributes def car_attr(name, colors, hp, length, width): car_attr = [name, colors, hp, length, width] print("The {} has the following specifications:".format(name)) print("Available colours: {}".format(colors)) print("HP: {}".format(hp)) print("Length: {}".format(length)) print("Width: {}".format(width)) return car_attr # function for the functionality def drive(model, mph): print("{} is driving at {} mph.".format(model, mph)) # In[12]: # specifying the model model_x = specify_model("Model X") # specifying the attributes model_x_attr = car_attr(name = model_x, colors = ["white", "black", "silver", "blue", "red"], hp = 443, length = 5.04, width = 2.27) # using the functionality drive(model_x, 100) # #### Object Oriented # Instance methods describe the functions of the objects of a class. They represent the operations (actions) that can be performed on these objects, and only these object. In other words, the defined functions cannot be called from instances other than the ones of that specific class. The execution of a method can lead to a change of the state of the object. # # Methods are defined in the same way as normal functions but must be declared within the body of the class. Their first argument always refers to the calling instance, thus methods are said to be functions, attached to objects. Similar to the `__innit__` method, the first parameter of the method is by convention always the name self. # `.drive()` has one additional parameter called mph and returns a string containing the car’s state (driving) and the velocity in mph. Of course, you could also implement a method that only uses the self parameter. Naturally, other methods regarding the car example could be: braking, parking, etc. # # # In[13]: class Car: #class atrributes brand = "Tesla" #instance attributes def __init__(self, name, colors, hp, length, width): self.name = name self.colors = colors self.hp = hp self.length = length self.width = width self.state = "off" # defining the functionalities def drive(self, mph): self.state = "driving" print("{} is {} at {} mph.".format(self.name, self.state, mph)) # In[14]: # instantiating the object model_x = Car(name = "Model X", colors = ["white", "black", "silver", "blue", "red"], hp = 443, length = 5.04, width = 2.27) # printing the name and class attribute print("The {} is a car by {}.".format(model_x.name, model_x.brand)) # printing mone instance attributes print("The {} has a length of {}m and a width of {}m.".format(model_x.name, model_x.length, model_x.width)) # using the functionality model_x.drive(100) # We have now specified the car, defined some attributes and finally added a functionality in all three programming paradigms. `Model_X` is now completed. # Especially regarding the object oriented programming, you have learnt how to: # - Define a class # - Instantiate an object # - Define attributes # - Define methods # # Although only the driving functionality has been added to our Model X, many other functionalities such as braking, lights on and lights off can be implemented. # # In the following, a second car will be built in order to illustrate the benefits of the individual paradigms in different situations. # # # ### Building a Different Car # # As mentioned above, a big advantage of OOP is its reusability. Classes can be reused for creating similar objects simply trough instantiating a new object of this class. Suppose we want to build Tesla's Model S now. The Model S has the same brand as the Model X, so the class attribute `brand` stays the same but the instance attributes: `name`, `colors`, `horsepower` (hp), `length`, `width`, etc. will have different values. # # Now, for building this diffent car all we have to do is reuse the syntax for instantiating an object and pass it the new values for the instance attributes. # In[15]: # instantiating the Model S model_s = Car(name = "Model S", colors = ["white", "black", "silver", "blue", "red"], hp = 670, length = 4.98, width = 2.19) # See, how easy that was? We can now have a look at our new car's attributes: # In[16]: # printing the class attributes print("The {} is a car by {}.".format(model_s.name, model_s.brand)) # printing some instance attributes print("The {} has a length of {}m and a width of {}m.".format(model_s.name, model_s.length, model_s.width)) # The new object automatically gets all the class functionalities as well. For our `Model_S` this means that just like the `Model_X` it has the method `Model_S.drive(mph)`. # In[17]: # using the functionality Car.drive() model_s.drive(100) # # ## Four Fundamental Principles of Object-oriented Programming # # ### Encapsulation # Encapsulation refers to the principle of keeping the state of each object private, inside a class. By limiting direct access to this state and only allowing the object's own public setter methods to modify state in the class, this prevents the unintentional spread of changes made in one part of a program to other parts of the program. Encapsulation is especially important in large and complex projects worked on by teams of programmers, where communication between different parts of the program must be carefully managed. # In[1]: class Gorilla2: def __init__(self, name, awakeness, colour): self.name = name self.awakeness = awakeness self.__colour = colour # Note the double underscore denoting a private attribute def currentstate(self): print(f"{self.name} is currently {self.awakeness}.") def currentcolour(self): print(f"{self.name} is {self.__colour} in colour.") def spraypaint(self, paint): # Public method that changes the private attribute "self.__colour" self.paint = paint self.__colour = paint kingkong = Gorilla2("King Kong", "asleep", "black") kingkong.currentstate() kingkong.awakeness = "awake" # Usually, attributes in a class can be directly modified outside the class kingkong.currentstate() print("") kingkong.currentcolour() kingkong.__colour = "brown" # However, private attributes cannot be modified outside the class kingkong.currentcolour() # Therefore, this attribute will not be changed to "brown" print("") kingkong.currentcolour() kingkong.spraypaint("red") # Private attributes can only be modified by public setter methods of the class kingkong.currentcolour() print("") # ### Abstraction # Abstraction refers to the principle of displaying essential information by hiding unnecessary information. This is done by creating sub-classes for this unnecessary information. This isolation of the information is similar to inheritance in the aspect of how it is achieved (creating specific sub-classes), but it has its own purpose: simplicity. # # This principle may not seem advantageous at first glance. After all, why would we want to hide information about how the tasks of our program are achieved? How exactly does this reduce complexity? Consider a TV remote. Do we know exactly how each of the buttons on our remote function in everyday usage? Should we be reminded at every press of a button on our remote, that we are making the underlying chip's sensor turn on, producing an electrical signal which is amplified with a transistor, then sent through a LED which finally prompts an infrared light to communicate with our TV? # # What matters to us is that the power button correctly prompts the television to turn on, that the volume button changes the volume, and that the channel changes when we use the button to change channels. The inner-workings of these buttons do not need to be apparent in the daily usage of our remote. This is the exact point of abstraction: we want to reduce complexity. Through abstraction we are also able to isolate parts of our code, making its maintenance more efficient by ensuring changes are to be made locally. # ### Inheritance # Inheritance refers to how an object-oriented programming language allows the creation of (child) subclasses using the characteristics of an existing parent or superclass. In other words, the child class can inherit attributes from the parent class. By simply inheriting from the parent class, we have inherited all its functionality. Python will travel up the inheritance chain (e.g. Gorilla to Monkey) until it finds the called method (init) to be executed. Now, let's customize our subclass a little bit. Adding methods for the child class is no different than adding methods for the parent class. It is not possible to remove inherited attributes, variables and methods from the subclass, as they do not exist in the subclass. However, it is possible to overwrite them, as shown in the example below. # In[2]: # This is our parent class class Monkey: family = "Monkey" # Instance attribute def __init__(self, name, age): self.name = name self.age = age # Instance method def eat(self): return '{} is eating a banana.'.format(self.name) # Creating our first child class class Gorilla(Monkey): species = 'Gorilla' # Adding a new attribute def __init__(self, name, age, strength): # Calling the parents init methods super().__init__(name, age) # Call the new subclass specific attribute self.strength = strength # Strength is quantified by how many kilograms the gorilla benches # Creating our second child class class Chimpanzee(Monkey): species = 'Chimpanzee' # Adding a new attribute def __init__(self, name, age, IQ): # Calling the parents init methods super().__init__(name, age) # Call the new subclass specific attribute self.IQ = IQ # Replace method def eat(self): return '{} is eating caviar.'.format(self.name) brutus = Gorilla('Brutus', 11, 130) george = Chimpanzee("George", 7, 170) print(brutus.__class__.family) print(brutus.__class__.species) print("") print("Brutus benches {} kg.".format(brutus.strength)) print("George has an IQ of {}.".format(george.IQ)) print("") print(brutus.eat()) print(george.eat()) # Brutus, our object of sub-class Gorilla has effectively inherited the family Monkey from its parent attribute. However he also has specific attributes, such as his species Gorilla. # # Both our monkeys Brutus and George have been given a new attribute, unique to their sub-class. Brutus has been given a strength one, whereas George an IQ one. While they both have the same parent class Monkey, they also have their own specific-sub classes which allow them to have different characteristics, gorillas being known for their strength and chimpanzees for their intelligence. # # Lastly, while Brutus has inherited the parent-class eat function, and thus eats bananas, George shows that it is possible to replace parent class methods. Chimpanzees are special and eat caviar. # ### Polymorphism # Polymorphism is one of the 4 pillars of OOP. It allows our program to process information differently based on their data type. This is achieved through the usage of a generic interface. Let's illustrate this with an example. # In[3]: # Creation first class class Dolphin: def sing(self): print("Dolphins can't sing, silly.") # Creation second class class Gorilla: def sing(self): print ("*Sings Despacito*") # Creation of generic interface def singing(animal): animal.sing() # Now let's test what this does # First, we create 2 animals, a dolphin and a gorilla dolphin1 = Dolphin() gorilla1 = Gorilla() # Pass our objects through the generic interface singing(dolphin1) singing(gorilla1) # We have defined two different classes, `Dolphin()` and `Gorilla()`, each with their own `sing()` method. We then defined a generic interface `singing()`, for the input of any object. Then, passing our two objects, `dolphin1` and `gorilla1` through this interface, we get differing output, depending on the class of our input. In the case of the `Gorilla`, it is able to sing. Because gorillas can sing. Duh. Well only Despacito. In the case of our `Dolphin`, it is sadly unable to sing. # # The generic interface serves a similar purpose as a switchboard. We are now able to enter the `sing()` function of both `Gorillas` and `Dolphins` into this interface, which then directs our program to the correct class, allowing it to differentiate the `singing()` function for our data type, depending on whether it is a `Gorilla` and `Dolphin`. Differentiation of our objects is the main strength of Polymorphism. # # The principles of Inheritance and Polymorphism go hand in hand. While Inheritance allows our sub-classes to inherit the _same_ attributes and methods as a parent or super-class, Polymorphism allows for these to __differ__ depending on which sub-class they originate from. # ## Sources # # **Overview of Programming Paradigms** # # * https://blog.newrelic.com/engineering/python-programming-styles/ # * https://hackr.io/blog/procedural-programming # * https://t3n.de/news/tesla-rueckruf-touchscreen-1353586/ # * https://www.freepik.com/free-photos-vectors/construction-blueprint # * https://www.programiz.com/python-programming/object-oriented-programming # * https://realpython.com/python3-object-oriented-programming/