Tuesday, March 29, 2022

Tuples

Tuples are usually used for a small number of entries and when the position and sequence of the entries in a collection is important. To preserve the sequence of entries, tuples are designed as immutable, and this is where tuples differentiate themselves from lists.

Operations on a tuple are typically faster than a regular list datatype. In cases when the values in a collection are required to be constant in a particular order, using tuples is the preferred option because of their superior performance.

Tuples are normally initialized with values because they are immutable. A simple tuple can be created using parenthesis. A few ways to create tuple instances are shown in the next code snippet:

w = () #an empty tuple
x = (2, 3) #tuple with two elements
y = ("Hello World") #not a tuple, Comma is required \ for single entry tuple
z = ("Hello World",) #A comma will make it a tuple

In this code snippet, we created an empty tuple (w), a tuple with numbers (x), and a tuple with the text Hello World, which is z. The variable y is not a tuple since, for a 1-tuple (a single-object tuple), we need a trailing comma to indicate that it is a tuple.

After introducing lists and tuples, we will briefly introduce dictionaries in the next post

Share:

Monday, March 28, 2022

Lists

The list is one of the basic collection types in Python, which is used to store multiple objects using a single variable. Lists are dynamic and mutable, which means the objects in a list can be changed and the list can grow or shrink.

List objects in Python are not implemented using any linked list concept but using a variable-length array. The array contains references to objects it is storing. The pointer of this array and its length are stored in the list head structure, which is kept up to date as objects are added or deleted from a list. The behavior of such an array is made to appear like a list but in reality, it is not a real list. That is why some of the operations on a Python list are not optimized. For example, inserting a new object into a list and deleting objects from a list will have a complexity of n.

To rescue the situation, Python provides a deque data type in the collections built-in module. The deque data type provides the functionality of stacks and queues and is a good alternative option for cases when a linked list-like behavior is demanded by a problem statement.

Lists can be created empty or with an initial value using square brackets. Next, we present a code snippet that demonstrates how to create an empty or non-empty list object using only the square brackets or using the list object constructor:

e1 = [] #an empty list
e2 = list() #an empty list via constructor
g1 = ['a', 'b'] #a list with 2 elements
g2 = list(['a', 'b']) #a list with 2 elements using a \constructor
g3 = list(g1) #a list created from a list

The details of the operations available with a list object, such as add, insert, append, and delete can be reviewed in the official Python documentation. I have already covered these in older posts.

Share:

Sunday, March 27, 2022

Python data containers

Python supports several data types, both numeric as well as collections. Defining numeric data types such as integers and floating-point numbers is based on assigning a value to a variable. The value we assign to a variable determines the type of the numeric data type. Note that a specific constructor (for example, int() and float()) can also be used to create a variable of a specific data type. Container data types can also be defined either by assigning values in an appropriate format or by using a specific constructor for each collection data type. We will study five different container data types: strings, lists, tuples, dictionaries, and sets.

Strings

Strings are not directly a container data type. But it is important to discuss the string data type because of its wide use in Python programming and also the fact that the string data type is implemented using an immutable sequence of Unicode code points. The fact that it uses a sequence (a collection type) makes it a candidate to be discussed in this post.

String objects are immutable objects in Python. With immutability, string objects provide a safe solution for concurrent programs where multiple functions may access the same string object and will get the same result back. This safety is not possible with mutable objects. Being immutable objects, string objects are popular to use as keys for the dictionary data type or as data elements for the set data type. The drawback of immutability is that a new instance needs to be created even if a small change is to be made to an existing string instance. 

String literals can be enclosed by using matching single quotes (for example, 'blah'), double quotes (for example, "blah blah"), or triple single or double quotes (for example, """none""" or '''none'''). It is also worth mentioning that string objects are handled differently in Python 3 versus Python 2. In Python 3, string objects can hold only text sequences in the form of Unicode data points, but in Python 2 they can hold text as well as byte data. In Python 3, byte data is handled by the bytes data type.

Separating text from bytes in Python 3 makes it clean and efficient but at the cost of data portability. The Unicode text in strings cannot be saved to disk or sent to a remote location on the network without converting it into a binary format. This conversion requires encoding the string data into a byte sequence, which can be achieved in one of the following ways:

• Using the str.encode (encoding, errors) method: This method is available on the string object and it can take two arguments. A user can provide the type of codec to be used (UTF-8 being the default) and how to handle the errors.

• Converting to the bytes datatype: A string object can be converted to the Bytes data type by passing the string instance to the bytes constructor along with the encoding scheme and the error handling scheme.

The details of methods and the attributes available with any string object can be found in the official Python documentation as per the Python release.

In the next post we will discuss about Lists

Share:

Saturday, March 26, 2022

When not to use OOP in Python

Python has the flexibility to develop programs using either OOP languages such as Java or using declarative programming such as C. OOP is always appealing to developers because it provides powerful tools such as encapsulation, abstraction, inheritance, and polymorphism, but these tools may not fit every scenario and use case. These tools are more beneficial when used to build a large and complex application, especially one that involves user interfaces (UIs) and user interactions.

If your program is more like a script that has to execute certain tasks and there is no need to keep the state of objects, using OOP is overkill. Data science applications and intensive data processing are examples where it is less important to use OOP but more important to define how to execute tasks in a certain order to achieve goals. A real-world example is writing client programs for executing data-intensive jobs on a cluster of nodes, such as Apache Spark for parallel processing. Here are a few more scenarios where using OOP is not necessary:

• Reading a file, applying logic, and writing back to a new file is a type of program that is easier to implement using functions in a module rather than using OOP.

• Configuring devices using Python is very popular and it is another candidate to be done using regular functions.

• Parsing and transforming data from one format to another format is also a use case that can be programmed by using declarative programming rather than OOP.

• Porting an old code base to a new one with OOP is not a good idea. We need to remember that the old code may not be built using OOP design patterns and we may end up with non-OOP functions wrapped in classes and objects that are hard to maintain and extend.

In short, it is important to analyze the problem statement and requirements first before choosing whether to use OOP or not. It also depends on which third-party libraries you will be using with your program. If you are required to extend classes from third-party libraries, you will have to go along with OOP in that case.

 

Share:

Friday, March 25, 2022

Duck typing

Duck typing, sometimes referred to as dynamic typing, is mostly adopted in programming languages that support dynamic typing, such as Python and JavaScript. The name duck typing is borrowed based on the following quote:

"If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck." This means that if a bird is behaving like a duck, it will likely be a duck. The point of mentioning this quote is that it is possible to identify an object by its behavior, which is the core principle of duck typing in Python.

In duck typing, the type of class of an object is less important than the method (behavior) it defines. Using duck typing, the types of the object are not checked, but the method that is expected is executed.
To illustrate this concept, we take a simple example with three classes, Car, Cycle, and Horse, and we try to implement a start method in each of them. In the Horse class, instead of naming the method start, we call it push. Here is a code snippet with all three classes and the main program at the end:

#ducttype1.py
class Car:
def start(self):
print ("start engine by ignition /battery")
class Cycle:
def start(self):
print ("start by pushing paddles")

class Horse:
def push(self):
print ("start by pulling/releasing the reins")
if __name__ == "__main__":
for obj in Car(), Cycle(), Horse():
obj.start()

In the main program, we try to iterate the instances of these classes dynamically and call the start method. As expected, the obj.start() line failed for the Horse object because the class does not have any such method. As we can see in this example, we can put different class or instance types in one statement and execute the methods across them.

If we change the method named push to start inside the Horse class, the main program will execute without any error. Duck typing has many use cases, where it simplifies the solutions. Use of the len method in many objects and the use of iterators are a couple of many examples.


Share:

Thursday, March 24, 2022

Composition

Composition is another popular concept in OOP that is again somewhat relevant to encapsulation. In simple words, composition means to include one or more objects inside an object to form a real-world object. A class that includes other class objects is called a composite class, and the classes whose objects are included in a composite class are known as component classes. In the following screenshot, we show an example of a composite class that has three component class objects, A, B, and C:

 

Composition is considered an alternative approach to inheritance. Both design approaches are meant to establish a relationship between objects. In the case of inheritance, the objects are tightly coupled because any changes in parent classes can break the code in child classes. On the other hand, the objects are loosely coupled in the case of composition, which facilitates changes in one class without breaking our code in another class. Because of the flexibility, the composition approach is quite popular, but this does not mean it is the right choice for every problem. How, then, can we determine which one to use for which problem? There is a rule of thumb for this. When we have an is a relationship between objects, inheritance is the right choice—for example, a car is a vehicle, and a cat is an animal. In the case of inheritance, a child class is an extension of a parent class, with additional functionality and the ability to reuse parent class functionality. If the relation between objects is that one object has another object, then it is better to use composition—for example, a car has a battery.

We will take our previous example of the Car class and the Engine class. In the example code for multiple inheritance, we implemented the Car class as a child of the Engine class, which is not really a good use case of inheritance. It's time to use composition by implementing the Car class with the Engine object inside the Car class. We can have another class for Seat and we can include it inside the Car class as well.

We will illustrate this concept further in the following example, in which we build a Car class by including Engine and Seat classes in it:

#composition1.py
class Seat:
def __init__(self, type):
self.i_type = type
def __str__(self):
return f"Seat type: {self.i_type}"
class Engine:
def __init__(self, size):
self.i_size = size
def __str__(self):
return f"Engine: {self.i_size}"
class Car:
def __init__(self, color, eng_size, seat_type): 

self.i_color = color
self.engine = Engine(eng_size)
self.seat = Seat(seat_type)
def print_me(self):
print(f"This car of color {self.i_color} with \
{self.engine} and {self.seat}")
if __name__ == "__main__":
car = Car ("blue", "2.5L", "leather" )
car.print_me()
print(car.engine)
print(car.seat)
print(car.i_color)
print(car.engine.i_size)
print(car.seat.i_type)

We can analyze this example code as follows:

1. We defined Engine and Seat classes with one attribute in each class: i_size for the Engine class and i_type for the Seat class.

2. Later, we defined a Car class by adding the i_color attribute, an Engine instance, and a Seat instance in it. The Engine and Seat instances were created at the time of creating a Car instance.

3. In this main program, we created an instance of Car and performed the following actions:
a) car.print_me: This accesses the print_me method on the Car instance.
b) print(car.engine): This executes the __str__ method of the Engine class.
c) print(car.seat): This executes the __str__ method of the Seat class.
d) print(car.i_color): This accesses the i_color attribute of the Car instance.
e) print(car.engine.i_size): This accesses the i_size attribute of the Engine instance inside the Car instance.
f) print(car.seat.i_type): This accesses the i_type attribute of the Seat instance inside the Car instance

The console output of this program is shown here:

This car of color blue with Engine: 2.5L and Seat type: leather
Engine: 2.5L
Seat type: leather
blue
2.5L
leather

In the next post, we will discuss duck typing, which is an alternative to polymorphism.

Share:

Wednesday, March 23, 2022

Abstraction

Abstraction is another powerful feature of OOP and is mainly related to hide the details of the implementation and show only the essential or high-level features of an object. A real-world example is a car that we derive with the main features available to us as a driver, without knowing the real details of how the feature works and which other objects are involved to provide these features.

Abstraction is a concept that is related to encapsulation and inheritance together, and that is why we have kept this topic till the end to understand encapsulation and inheritance first. Another reason for having this as a separate topic is to emphasize the use of abstract classes in Python.

Abstract classes in Python

An abstract class acts like a blueprint for other classes. An abstract class allows you to create a set of abstract methods (empty) that are to be implemented by a child class. In simple terms, a class that contains one or more abstract methods is called an abstract class. On the other hand, an abstract method is one that only has a declaration but no implementation.

There can be methods in an abstract class that are already implemented and that can be leveraged by a child class (as is) using inheritance. The concept of abstract classes is useful to implement common interfaces such as application programming interfaces (APIs) and also to define a common code base in one place that can be reused by child classes.

An abstract class can be implemented using a Python built-in module called Abstract Base Classes (ABC) from the abc package. The abc package also includes the Abstractmethod module, which utilizes decorators to declare the abstract methods.

A simple Python example with the use of the ABC module and the abstractmethod decorator is shown next:

#abstraction1.py
from abc import ABC, abstractmethod
class Vehicle(ABC):
def hello(self):
print(f"Hello from abstract class")
@abstractmethod
def print_me(self):
pass
class Car (Vehicle):
def __init__(self, color, seats):
self.i_color = color
self.i_seats = seats
"""It is must to implemented this method"""
def print_me(self):
print( f"Car with color {self.i_color} and no of \
seats {self.i_seats}")
if __name__ == "__main__":
# vehicle = Vehicle() #not possible
# vehicle.hello()
car = Car ("blue", 5)
car.print_me()
car.hello() 

In this example, we did the following:

• We made the Vehicle class abstract by inheriting it from the ABC class and also by declaring one of the methods (print_me) as an abstract method. We used the @abstractmethod decorator to declare an abstract method.

• Next, we updated our famous Car class by implementing the print_me method in it and keeping the rest of the code the same as in the previous example.

• In the main part of the program, we attempted to create an instance of the Vehicle class (code commented in the illustration). We created an instance of the Car class and executed the print_me and hello methods.

When we attempt to create an instance of the Vehicle class, it gives us an error like this:

Can't instantiate abstract class Vehicle with abstract methods
print_me

Also, if we try to not implement the print_me method in the Car child class, we get an error. For an instance of the Car class, we get the expected console output from the print_me and hello methods.

Our next topic of discussion will be composition.

Share:

Tuesday, March 22, 2022

Polymorphism

In OOP, polymorphism is the ability of an instance to behave in multiple ways and a way to use
the same method with the same name and the same arguments, to behave differently in accordance with the class it belongs to.

Polymorphism can be implemented in two ways: method overloading and method overriding. Let us discuss each of them.

Method overloading

Method overloading is a way to achieve polymorphism by having multiple methods with the same name, but with a different type or number of arguments. There is no clean way to implement method overloading in Python. Two methods cannot have the same name in Python. In Python, everything is an object, including classes and methods. When we write methods for a class, they are in fact attributes of a class from the namespace perspective and thus cannot have the same name. If we write two methods with the same name, there will be no syntax error, and the second one will simply replace the first one.

Inside a class, a method can be overloaded by setting the default value to the arguments. This is not the perfect way of implementing method overloading, but it works. Here is an example of method overloading inside a class in Python:

#methodoverloading1.py
class Car:
def __init__(self, color, seats):
self.i_color = color
self.i_seat = seats
def print_me(self, i='basic'):
if(i =='basic'):
print(f"This car is of color {self.i_color}")
else:
print(f"This car is of color {self.i_color} \

with seats {self.i_seat}")
if __name__ == "__main__":
car = Car("blue", 5 )
car.print_me()
car.print_me('blah')
car.print_me('detail')

In this example, we add a print_me method with an argument that has a default value. The default value will be used when no parameter will be passed. When no parameter is passed to the print_me method, the console output will only provide the color of the Car instance. When an argument is passed to this method (regardless of the value), we have a different behavior of this method, which is providing both the color and the number of seats of the Car instance. Here is the console output of this program
for reference:

This car is of color blue
This car is of color blue with seats 5
This car is of color blue with seats 5 

Method overriding

Having the same method name in a child class as in a parent class is known as method overriding. The implementation of a method in a parent class and a child class is expected to be different. When we call an overriding method on an instance of a child class, the Python interpreter looks for the method in the child class definition, which is the overridden method. The interpreter executes the child class-level method. If the interpreter does not find a method at a child instance level, it looks for it in a parent class. If we have to specifically execute a method in a parent class that is overridden in a child class using the child class instance, we can use the super() method to access the parent class-level method. This is a more popular polymorphism concept in Python as it goes hand in hand with inheritance and is one of the powerful ways of implementing inheritance.

To illustrate how to implement method overriding, we will update the inhertance1.py snippet by renaming the print_vehicle_info method name as print_me. As we know, print_me methods are already in the two child classes with different implementations. Here is the updated code with the changes highlighted:

#methodoverriding1.py
class Vehicle:
def __init__(self, color):
self.i_color = color
def print_me(self):
print(f"This is vehicle and I know my color is \
{self.i_color}")
class Car (Vehicle):
def __init__(self, color, seats):
self.i_color = color
self.i_seats = seats
def print_me(self):
print( f"Car with color {self.i_color} and no of \
seats {self.i_seats}")
class Truck (Vehicle):
def __init__(self, color, capacity):
self.i_color = color
self.i_capacity = capacity
def print_me(self):
print( f"Truck with color {self.i_color} and \
loading capacity {self.i_capacity} tons")
if __name__ == "__main__":
vehicle = Vehicle("red")
vehicle.print_me()
car = Car ("blue", 5)
car.print_me()

truck = Truck("white", 1000)
truck.print_me()

In this example, we override the print_me method in the child classes. When we create three different instances of Vehicle, Car, and Truck classes and execute the same method, we get different behavior. Here is the console output as a reference:

This is vehicle and I know my color is red
Car with color blue and no of seats 5
Truck with color white and loading capacity 1000 tons

Method overriding has many practical applications in real-world problems—for example, we can  inherit the built-in list class and can override its methods to add our functionality. Introducing a custom sorting approach is an example of method overriding for a list object.

In the next post we will discuss about Abstraction.


Share:

Monday, March 21, 2022

Extending classes with inheritance

The concept of inheritance in OOP is similar to the concept of inheritance in the real world, where children inherit some of the characteristics from their parents on top of their own characteristics.

Similarly, a class can inherit elements from another class. These elements include attributes and methods. The class from which we inherit another class is commonly known as a parent class, a superclass, or a base class. The class we inherit from another class is called a derived class, a child class, or a subclass. The following screenshot shows a simple relationship between a parent class and a child class:


In Python, when a class inherits from another class, it typically inherits all the elements that compose the parent class, but this can be controlled by using naming conventions (such as double underscore) and access modifiers.

Inheritance can be of two types: simple or multiple. Let us discuss these.

Simple inheritance

In simple or basic inheritance, a class is derived from a single parent. This is a commonly used inheritance form in OOP and is closer to the family tree of human beings. The syntax of a parent class and a child class using simple inheritance is shown next:

class BaseClass:

<attributes and methods of the base class >

class ChildClass (BaseClass):

<attributes and methods of the child class >

For this simple inheritance, we will modify our example of the Car class so that it is derived from a Vehicle parent class. We will also add a Truck child class to elaborate on the concept of inheritance. Here is the code with modifications:

#inheritance1.py

class Vehicle:

def __init__(self, color):

self.i_color = color

def print_vehicle_info(self):

print(f"This is vehicle and I know my color is \

{self.i_color}")

class Car (Vehicle):

def __init__(self, color, seats):

self.i_color = color

self.i_seats = seats

def print_me(self):

print( f"Car with color {self.i_color} and no of \

seats {self.i_seats}")

class Truck (Vehicle):

def __init__(self, color, capacity):

self.i_color = color

self.i_capacity = capacity

def print_me(self):

print( f"Truck with color {self.i_color} and \

loading capacity {self.i_capacity} tons")

if __name__ == "__main__":

car = Car ("blue", 5)

car.print_vehicle_info()

car.print_me()

truck = Truck("white", 1000)

truck.print_vehicle_info()

truck.print_me()

In this example, we created a Vehicle parent class with one i_color attribute and one print_vehicle_info method. Both the elements are a candidate for inheritance.

Next, we created two child classes, Car and Truck. Each child class has one additional attribute (i_seats and i_capacity) and one additional method (print_me). In the print_me methods in each child class, we access the parent class instance attribute as well as child class instance attributes.

This design was intentional, to elaborate the idea of inheriting some elements from the parent class and adding some elements of its own in a child class. The two child classes are used in this example to demonstrate the role of inheritance toward reusability. In our main program, we created Car and Truck instances and tried to access the parent method as well as the instance method. The console output of this program is as expected and is shown below:

This is vehicle and I know my color is blue

Car with color blue and no of seats 5

This is vehicle and I know my color is white

Truck with color white and loading capacity 1000 tons






Share:

Sunday, March 20, 2022

Using property decorators

Using a decorator to define getters and setters is a modern approach that helps to achieve the Python way of programming.

If you are into using decorators, then we have a @property decorator in Python to make the code simpler and cleaner. The Car class with traditional getters and setters is updated with decorators, and here is a code snippet showing this:

carexample7.py

class Car:

__mileage_units = "Mi"

def __init__(self, col, mil):

self.__color = col

self.__mileage = mil

def __str__(self):

return f"car with color {self.color} and mileage \

{self.mileage}"

@property

def color(self):

return self.__color

@property

def mileage(self):

return self.__mileage

@mileage.setter

def mileage (self, new_mil):

self.__mileage = new_mil

if __name__ == "__main__":

car = Car ("blue", 1000)

print (car)

print (car.color)

print(car.mileage)

car.mileage = 2000

print (car.color)

print(car.mileage)

In this updated class definition, we updated or added the following:

• Instance attributes as private variables

• Getter methods for color and mileage by using the name of the attribute as the method name and using @property

• Setter methods for mileage using the @mileage.setter decorator, giving the method the same name as the name of the attribute

In the main script, we access the color and the mileage attributes by using the instance name followed by a dot and the attribute name (the Pythonic way). This makes the code syntax concise and readable. The use of decorators also makes the name of the methods simpler.

So we have discussed all aspects of encapsulation in Python, using classes for the bundling of data and actions, hiding unnecessary information from the outside world of a class, and how to protect data in a class using getters, setters, and property features of Python. In the next post, we will discuss how inheritance is implemented in Python.

Share:

Saturday, March 19, 2022

Protecting the data

We have seen in our previous code examples that we can access the instance attributes without any restrictions. We also implemented instance methods and we have no restriction on the use of these. We emulate to define them as private or protected, which works to hide the data and actions from the outside world.

But in real-world problems, we need to provide access to the variables in a way that is controllable and easy to maintain. This is achieved in many object-oriented languages through access modifiers such as getters and setters, which are defined next:

• Getters: These are methods used to access the private attributes from a class or its instance

• Setters: These are methods used to set the private attributes of a class or its instance.

Getters and setters methods can also be used to implement additional logic of accessing or setting the attributes, and it is convenient to maintain such an additional logic in one place. There are two ways to implement the getters and setters methods: a traditional way and a decorative way.

Using traditional getters and setters

Traditionally, we write the instance methods with a get and set prefix, followed by the underscore and the variable name. We can transform our Car class to use the getter and setter methods for instance attributes, as follows:

#carexample6.py

class Car:

__mileage_units = "Mi"

def __init__(self, col, mil):

self.__color = col

self.__mileage = mil

def __str__(self):

return f"car with color {self.get_color()} and \

mileage {self.get_mileage()}"

def get_color(self):

return self.__color

def get_mileage(self):

return self.__mileage

def set_mileage (self, new_mil):

self.__mileage = new_mil

if __name__ == "__main__":

car = Car ("blue", 1000)

print (car)

print (car.get_color())

print(car.get_mileage())

car.set_mileage(2000)

print (car.get_color())

print(car.get_mileage())

In this updated Car class, we added the following:

• color and mileage instance attributes were added as private variables.

• Getter methods for color and mileage instance attributes.

• A setter method only for the mileage attribute because color usually doesn't change once it is set at the time of object creation.

• In the main program, we get data for the newly created instance of the class using getter methods. Next, we updated the mileage using a setter method, and then we got data again for the color and mileage attributes.

The console output of each statement in this example is trivial and as per expectations. As mentioned, we did not define a setter for each attribute, but only for those attributes where it makes sense and the design demands. Using getters and setters is a best practice in OOP, but they are not very popular in Python. The culture of Python developers (also known as the Pythonic way) is still to access attributes directly.

Share:

Friday, March 18, 2022

Hiding information

We have seen in our previous code examples that we have access to all class-level as well as instance-level attributes without any restrictions. Such an approach led us to a flat design, and the class will simply become a wrapper around the variables and methods. A better object-oriented design approach is to hide some of the instance attributes and make only the necessary attributes visible to the outside world. To discuss how this is achieved in Python, we introduce two terms: private and protected.

Private variables and methods

A private variable or attribute can be defined by using a double underscore as a prefix before a variable name. In Python, there is no keyword such as private, as we have in other programming languages. Both class and instance variables can be marked as private.

A private method can also be defined by using a double underscore before a method name. A private method can only be called within the class and is not available outside the class.

Whenever we define an attribute or a method as private, the Python interpreter doesn't allow access for such an attribute or a method outside of the class definition. The restriction also applies to subclasses; therefore, only the code within a class can access such attributes and methods.

Protected variables and methods

A protected variable or a method can be marked by adding a single underscore before the attribute name or the method name. A protected variable or method should be accessed or used by the code written within the class definition and within subclasses—for example, if we want to convert the i_color attribute from a public to a protected attribute, we just need to change its name to _i_color. The Python interpreter does not enforce this usage of the protected elements within a class or subclass. It is more to honor the naming convention and use or access the attribute or methods as per the definition of the protected variables and methods.

By using private and protected variables and methods, we can hide some of the details of the implementation of an object. This is helpful, enabling us to have a tight and clean source code inside a large-sized class without exposing everything to the outside world.

Another reason for hiding attributes is to control the way they can be accessed or updated. This is a topic for the next post. To conclude this post, we will discuss an updated version of our Car class with private and protected variables and a private method, which is shown next:

#carexample5.py

class Car:

c_mileage_units = "Mi"

__max_speed = 200

def __init__(self, color, miles, model):

self.i_color = color

self.i_mileage = miles

self.__no_doors = 4

self._model = model

def __str__(self):

return f"car with color {self.i_color}, mileage

{self.i_mileage}, model {self._model} and doors

{self.__doors()}"

def __doors(self):

return self.__no_doors

if __name__ == "__main__":

car = Car ("blue", 1000, "Camry")

print (car)

In this updated Car class, we have updated or added the following as per the previous example:

• A private __max_speed class variable with a default value

• A private __no_doors instance variable with a default value inside the __init__constructor method

• A _model protected instance variable, added for illustration purposes only

• A __doors() private instance method to get the number of doors

• The __str__ method is updated to get the door by using the __doors() private method

The console output of this program works as expected, but if we try to access any of the private methods or private variables from the main program, it is not available, and the Python interpreter will throw an error. This is as per the design, as the intended purpose of these private variables and private methods is to be only available within a class.

For the Car class example, we can access the private variables and private methods. Python provides access to these attributes and methods outside of the class definition with a different attribute name that is composed of a leading underscore, followed by the class name, and then a private attribute name. In the same way, we can access the private methods as well.

The following lines of codes are valid but not encouraged and are against the definition of private and protected:

print (Car._Car__max_speed)

print (car._Car__doors())

print (car._model)

As we can see, _Car is appended before the actual private variable name. This is done to minimize the conflicts with variables in inner classes as well.

Share: