Stack

What is a stack?

Concept of Object Oriented Programming - Coins Stacking

A stack is a data structure that follows the Last In First Out (LIFO) principle. This means that the last element that is added to the stack is the first element that is removed.

A stack can be visualized as a stack of coins. You can only add a coin to the top of the stack, and you can only remove a coin from the top of the stack.

The two basic operations of a stack

The two basic operations of a stack are push and pop.

  • Push: Push is used to add an element to the stack. The element is added to the top of the stack.
  • Pop: Pop is used to remove an element from the stack. The element is removed from the top of the stack.

How to use a stack?

A stack can be used to keep track of the order in which you have visited websites. You can push the URL of the website you are currently visiting onto the stack, and then pop the URL of the website you want to visit next.

A stack can also be used to reverse the order of a list of numbers. You can push each number onto the stack, and then pop the numbers off the stack in reverse order.

Stack - The Procedural Approach

To create a stack using the procedural approach, we first need to create a list. In this section, we will show you how to create a stack using a list in Python.

To implement a stack, we first need to create a list. The following code creates a list called stack:

stack = []

We can then define a function called push() to add an element to the stack. The push() function takes one parameter, which is the element to be added to the stack. The following code defines the push() function:

def push(val):
   stack.append(value)

The push() function simply appends the parameter's value to the end of the stack.

We can also define a function called pop() to remove an element from the stack. The pop() function does not take any parameters. The following code defines the pop() function:

def pop():
   val = stack[-1]
   del stack[-1]
   return val

The pop() function removes the element from the top of the stack and returns it.

The following code shows how to use the push() and pop() functions to add and remove elements from a stack:

stack = []

def push(val):
    stack.append(val)

def pop():
    val = stack[-1]
    del stack[-1]
    return val

push(3)
push(2)
push(1)

print(pop())
print(pop())
print(pop())

This code first creates an empty stack called stack. Then, it pushes the numbers 1, 2, and 3 onto the stack. The print() statements then print the contents of the stack. Finally, the pop() function removes the top element from the stack and prints it. The output of the code is as shown below:

OUTPUT:

The procedural approach to implementing a stack is straightforward and easy to understand. However, it can be inefficient for large stacks. A more efficient approach is to implement a stack using a linked list.

Stacks in Python: Procedural vs. Object-Oriented Approaches

In the previous section, we learned how to implement a stack using the procedural approach. In addition, there are two main approaches to implementing a stack in Python: procedural and object-oriented.

The Procedural Approach

The procedural approach is the simplest way to implement a stack. It involves creating a list to store the elements of the stack, and then defining two functions: push() and pop(). The push() function adds an element to the stack, and the pop() function removes an element from the stack.

The following code shows how to implement a stack using the procedural approach:

def push(val):
  stack.append(val)

def pop():
  val = stack[-1]
  del stack[-1]
  return val

stack = []

push(1)
push(2)
push(3)

print(stack)  # Prints [1, 2, 3]

value = pop()
print(value)  # Prints 3

print(stack)  # Prints [1, 2]

The procedural approach is easy to understand and implement, but it has some limitations. For example, the stack is NOT ENCAPSULATED, which means that it is not protected from accidental modification. Additionally, the procedural approach is not scalable, which means that it can be difficult to use for large stacks.

The Object-Oriented Approach

The object-oriented approach is a more sophisticated way to implement a stack. It involves creating a class called Stack that encapsulates the data and methods of the stack. The Stack class has two methods: push() and pop(). The push() method adds an element to the stack, and the pop() method removes an element from the stack.

The following code shows how to implement a stack using the object-oriented approach (we will go through this in detail later):

class Stack:

  def __init__(self):
    self.items = []

  def push(self, value):
    self.items.append(value)

  def pop(self):
    return self.items.pop()

stack = Stack()

stack.push(1)
stack.push(2)
stack.push(3)

print(stack.items)  # Prints [1, 2, 3]

value = stack.pop()
print(value)  # Prints 3

print(stack.items)  # Prints [1, 2]

The object-oriented approach is more complex than the procedural approach, but it has several advantages. First, the stack is encapsulated, which means that it is protected from accidental modification. Second, the object-oriented approach is scalable, which means that it can be used for large stacks. Finally, the object-oriented approach is more flexible, which means that it can be easily extended to support additional features.

In general, the object-oriented approach is the preferred way to implement a stack in Python. However, the procedural approach may be a better choice for simple applications.

Stacks in Python: The Object-Oriented Approaches

Imagine you want to create something called a "stack" in a computer program. It's like a collection of items arranged on top of each other, and you can add or remove items from the top. To make this work, you need a place to store these items.

So, we're going to use a list to store these items in our program. But how do we make this list work as part of our stack idea?

Creating A Class

We start by creating a special thing called a "class" in the program. Think of a class as a blueprint that tells the computer how to create and manage our stack.

Here's where it starts:

class Stack:

Now, we have two important things we want from this class:

  • We want the class to have a place to store our stack's items. For every stack we create, we need a separate place to keep its items. This place should be like a private spot only for that stack.
  • We want to hide this place from anyone using our class. They shouldn't see or mess with this storage directly.
Stack class

Python, unlike some other programming languages, doesn't automatically provide a special place to store our stack's items. So, we need to create this place ourselves inside the class.

Here's how we do it:

class Stack:
  def __init__(self):
      self.stack_list = []


This might look a bit strange, but it's like telling the computer:

"Every time you create a new stack, make a special storage called stack_list just for it. And keep it hidden from others."

Stack class

The __init__ function is like a special helper that gets called automatically when a new stack is created. It's responsible for setting up the stack's storage.

In simple words, think of it like setting up a new room (storage) inside a building (class) for each stack you create.

So, to sum it up: We're creating a class for our stack. Inside the class, we make sure each stack has its own private storage using the __init__ function. This way, we're making sure that every time someone wants to use a stack, it comes with its own hidden storage space.

Constructor

A constructor is a special kind of method that is called when an object is created. It is used to initialize the object and set up its initial state.

The constructor is always named __init__ in Python. It takes at least one parameter, which conventionally represents the instance of the object itself and is typically named self.

The constructor can be used to do things like:

  • Create and initialize the object's properties
  • Perform any other necessary initialization

So, from the above example, we are actually creating a constructor.

The constructor is used to create a private storage space for each stack object. This storage space is not directly accessible from outside the class, which helps protect the integrity of the stack's data.

Stack class

Example: Defining a Simple Constructor

Let's add a very simple constructor to the Stack class we've been working on. In this example, we'll add an initialization message to the constructor:

class Stack:
    def __init__(self):
        self.stack_list = []
        print("Hi! A new stack is created.")

# Creating an instance of the Stack class
stack_object = Stack()

In the example:

  • The __init__ method is the constructor.
  • The parameter self refers to the instance of the Stack object being created.
  • Inside the constructor, we initialize the self.stack_list attribute to an empty list to create a private storage for each stack.
  • We also print a message to indicate that a new stack is created.

Note: The use of self as the first parameter in the constructor is a convention, and it simplifies the readability and understanding of your code. While it's not a strict requirement, it's highly recommended to follow this convention. Try to run the code, and you will get the output as shown below:

OUTPUT:

In the next section, we'll delve into the concept of instantiation, where we'll explore how to create instances of a class and how the constructor plays a crucial role in that process.

Instantiation

Instantiation is the process of creating an object from a class. When an object is instantiated, the constructor of the class is called. The constructor is responsible for initializing the object and setting up its initial state.

In the example above, the following code instantiates a Stack object:

stack_object = Stack()

This code calls the __init__() method of the Stack class. The __init__() method initializes the stack_list attribute to an empty list and prints a message to indicate that a new stack is created.

The __init__() method is always called when an object is instantiated. This means that the stack_list attribute will be initialized for every Stack object that is created. This ensures that each Stack object has its own private storage space.

Instantiation is a fundamental concept in OOP. It is the process of creating objects from classes, and it is how we can use the features of a class in our code.

Here are some of the benefits of instantiation:

  • It allows us to create objects that are tailored to our specific needs.
  • It allows us to reuse code.
  • It makes our code more modular and maintainable.

Encapsulating Data: Making Variables Private

In the previous section, we learned about the object-oriented approaches to implementing a stack in Python. In this section, we will continue our discussion on how to encapsulate data in a stack class by making variables private.

Private Variables

In Python, variables that are prefixed with two underscores (__) are considered private. This means that they cannot be accessed from outside the class.

For example, the following code defines a stack class with a private variable called __stack_list:

class Stack:
    def __init__(self):
        self.__stack_list = []

The __stack_list variable can only be accessed from within the Stack class. If you try to access it from outside the class, you will get an AttributeError exception. For instance, the code below will give you an ERROR.

class Stack:
    def __init__(self):
        self.__stack_list = []
        print("Hi! A new stack is created.")

# Creating an instance of the Stack class
stack_object = Stack()
print(stack_object.__stack_list)

Why Make Variables Private?

There are several reasons why you might want to make variables private.

  • To protect data: By making variables private, you can prevent accidental changes to the data. This can help to ensure the integrity of the data.
  • To enforce encapsulation: Encapsulation is the principle of hiding the implementation details of an object from the outside world. By making variables private, you are enforcing encapsulation and preventing other code from directly accessing the data.
  • To improve readability: Private variables are not visible to the user, which can make your code more readable and easier to understand.

When to Make Variables Private

Not all variables should be private. You should only make variables private if you need to protect them from accidental changes or if you want to enforce encapsulation.

For example, in the stack class, the __stack_list variable should be private because it stores the data for the stack. This data should be protected from accidental changes.

Later, we will add two methods to the Stack class: push() and pop(). How are we going to treat these methods? Unlike the constructor, both methods in the Stack class do not need to be private. These methods are used to manipulate the data in the stack, and it is okay for other code to call them.

In brief, encapsulating data by making variables private is a fundamental principle of object-oriented programming. It helps to protect data, enforce encapsulation, and improve readability.

In the next section, we will discuss methods in more detail and discuss how they contribute to the functionality of classes.

Inheritance - Expanding Functionality: Creating Subclasses and Overriding Method

Now, let's take a closer look at how we can enhance the capabilities of our classes by creating subclasses and overriding methods. This will allow us to build on the foundation we've already established and add more power to our code while keeping things organized and efficient.

Subclasses: Building on Existing Classes

Imagine you want to create a new type of stack that can do more than just store and retrieve values. Instead of starting from scratch, you can create a specialized version of the existing Stack class. This specialized version is called a subclass. A subclass inherits attributes and methods from its parent class (also known as the superclass) and can add its own unique features.

Creating the Subclass: Introducing the AddingStack

Concept of Object Oriented Programming - Coins Stacking

Let's say we want to create a subclass called AddingStack. This new type of stack will not only store values but also keep track of their sum. To begin, we define the AddingStack class and have it inherit from the Stack class:


class AddingStack(Stack):
      pass

In this code:

  • We define the AddingStack class and indicate that it inherits from the Stack class using (Stack).

Adding a New Feature: Summing Up Values

Our AddingStack class needs to calculate and maintain the sum of all the values in the stack. To do this, we'll add a private attribute named __sum. Similar to the private __stack_list attribute, we'll keep __sum hidden from direct access to protect its integrity:

class AddingStack(Stack):
      def __init__(self):
          Stack.__init__(self)  # Initialize the Stack's constructor
          self.__sum = 0  # Initialize the __sum variable

In this code:

  • We invoke the constructor of the superclass, Stack, using Stack.__init__(self). This ensures that the __stack_list attribute is set up.
  • We introduce a new private attribute, __sum, which will keep track of the sum of values.

Overriding Methods: Adding Special Behavior

To incorporate the summing functionality, we'll override the push() and pop() methods inherited from the Stack class:

class AddingStack(Stack):
      def __init__(self):
          Stack.__init__(self)
          self.__sum = 0

      def push(self, val):
          self.__sum += val  # Update the sum
          Stack.push(self, val)  # Call the Stack's push method

      def pop(self):
          val = Stack.pop(self)  # Call the Stack's pop method
          self.__sum -= val  # Update the sum
          return val

In this code:

  • We override the push() method to both push the value onto the stack and update the sum with the new value.
  • We call the push() method from the superclass, Stack, using Stack.push(self, val).
  • Similarly, we override the pop() method to subtract the popped value from the sum and then perform the usual pop operation.

Getting the Sum: Providing Access

To access the sum without exposing the private __sum attribute, we'll create a method named get_sum() that simply returns the value of __sum:

class AddingStack(Stack):
      def __init__(self):
          Stack.__init__(self)
          self.__sum = 0

      def push(self, val):
          self.__sum += val
          Stack.push(self, val)

      def pop(self):
          val = Stack.pop(self)
          self.__sum -= val
          return val

      def get_sum(self):
          return self.__sum

In this code:

  • We define a new method, get_sum(), which provides access to the value of __sum.

Putting It into Action: Using the New Subclass

Now that we've set up the AddingStack class, let's see it in action:

# Create instances of AddingStack
  adding_stack_1 = AddingStack()
  adding_stack_2 = AddingStack()

  # Push values onto the stacks
  adding_stack_1.push(3)
  adding_stack_2.push(5)

  # Print the sums of the stacks
  print(adding_stack_1.get_sum())  # Output: 3
  print(adding_stack_2.get_sum())  # Output: 5

In this code:

  • We create two instances of the AddingStack class.
  • We push values onto both stacks.
  • We use the get_sum() method to print the sums of the stacks.

Conclusion

In this section, we've explored the concept of creating subclasses and overriding methods to enhance the functionality of our classes. By building on existing classes, we can save time and effort while adding new features. Subclasses allow us to tailor classes to specific needs without altering the original code. In the next section, we'll dive deeper into inheritance, exploring various aspects of class relationships and how they contribute to building versatile and efficient code structures.