Python 3x Pandas Django

Encapsulation


It describes the idea of wrapping data and the methods that work on data within one unit. This puts restrictions on accessing variables and methods directly and can prevent the accidental modification of data.

A class is an example of encapsulation as it's encapsulates all the data that is variables, member functions, etc.

Example 1:

class Class1Students:
    def __init__(self, name, age):
        self.name = name
        self.age  = age

    def speak(self):
        print(f"my name is {self.name}, and I am {self.age} years old.")

student1 = Class1Students("Michael", 40)
student1.speak()                #Output: my name is Michael, and I am 40 years old.
print(student1.age)             #Output: 40

In the above example, Class1Students encapsulate the functionality of the member function speak() and variables name & age, and others can get access to these functionalities by this class object.

Strings is another example of encapsulation because of this encapsulation, we have all built-in string functionality like capitalize(), isalnum() methods from string class are available to us for use.

Example 2:

print("anycharacters".capitalize())
print("anycharacters".isalnum())

Some real-life example

Consider a real-life example of encapsulation, In a school, there are three different classes like class1, class2, and class3. The class1 teacher handles all the students in class1 and keeps records of all the data related to class1 students. Similarly, the class2 teacher handles all the students in class2 and keeps records of all the class2 students and similarly for class3. Now there may arise a situation a principal from the school needs all the data about students across the classes.

In this case, he is not allowed to directly access the data of the class1, 2, & 3. He will first have to contact teachers in these classes and then request them to give the particular data. This is what encapsulation is. Here the data of the class1 and the students that can manipulate them are wrapped under a single name “class1”.

Using encapsulation also hides the data. In this example, the data of the sections like class1, class2, or class3 are hidden from any other section.

Private vs Public Variables

"Private" instance variables that cannot be accessed except from inside an object don’t exist in Python. However, there is a convention that is followed by most Python code: a name prefixed with an underscore (e.g. _spam) should be treated as a non-public part of the API (whether it is a function, a method or a data member). It should be considered an implementation detail and subject to change without notice.

With the reference from Python Docs

Example 1

class Class1Students:
    def __init__(self, name, age):
        self.name = name
        self.age  = age

    def speak(self):
        print(f"my name is {self.name}, and I am {self.age} years old.")

student1 = Class1Students("Michael", 40)

#change the attributes
student1.name = "John"
student1.age  = 25 

student1.speak()                #Output: my name is John, and I am 25 years old.

In the above encapsulate example, attributes name & age updated by using a class object even though this class encapsulates these variables and member function but these variables\attributes and member functions are public.

Private Variables

In Python, we denote private attributes using underscore as the prefix i.e single _ or double __ .

With the reference from Python Docs. There are no true private variables As a python programmer we decided that _ underscore means we should not modify these variables and this should be considered as private variables that is how we achieved privacy in python.

Example 2

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

    def speak(self):
        print(f"my name is {self._name}, and I am {self._age} years old.")

student1 = Class1Students("Michael", 40)

#change the attributes
student1._name = "John"
student1._age  = 25 

student1.speak()                #Output: my name is John, and I am 25 years old.

It still is possible to access or modify a variable that is considered private(_name & _age). let see with an example __name & __age.

Example 3

Encapsulation is the mechanism for restricting the access to some of an object's components, this means that the internal representation of an object can't be seen from outside of the objects definition. Access to this data is typically only achieved through special methods: Getters and Setters. By using Getters and Setters methods, we can make sure that the internal data cannot be accidentally set into an inconsistent or invalid state.

class Class1Students:
    def __init__(self, name, age):
        self.__name = name
        self.__age   = age

    def speak(self):
        print(f"my name is {self.__name}, and I am {self.__age} years old.")

    #Setter Function
    def setNameAge(self, name, age):
        self.__name = name
        self.__age   = age

student1 = Class1Students("Michael", 40)

#change these attributes
student1.__name = "John"
student1.__age  = 25

student1.speak()                #Output: my name is Michael, and I am 40 years old.

#using setter function
student1.setNameAge('John', 25)
student1.speak()                #Output: my name is John, and I am 25 years old.

In the above example, We tried to modify the name and age. However, we can't change it because Python treats the __name & __age as private attributes.

To change the value, we have to use a setter function i.e setNameAge() which takes price as a parameter. so, there is not private variables in Python.

Dunder or Magic\Special Methods in Python

Dunder or magic methods in Python are the methods having two prefix and suffix underscores in the method name.

Dunder here means "Double Under (Underscores)". These are commonly used for operator overloading.

Some of them are: __init__, __add__, __len__, __repr__ etc. Use the dir() function to see the number of magic methods inherited by a class.

Example 1

print(dir(int))

Output:

['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__',
'__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__',
'__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__',
'__getnewargs__', '__gt__', '__hash__', '__index__', '__init__',
'__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__',
'__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__',
 '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__',
 '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__',
 '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__',
 '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__',
 '__truediv__', '__trunc__', '__xor__', 'bit_length', 'conjugate', 'denominator',
 'from_bytes', 'imag', 'numerator', 'real', 'to_bytes']

Note: dir() is a powerful inbuilt function in Python3, which returns list of the attributes and methods of any object.

We usually never write a dunder method and never write double underscore like this __ for our variables or member functions.

There are lots of dunder methods in each objects(int, float, str, list, tuple...) that have some special meaning in python and the reason that __ is because they are saying if you writing __ for your variables, you are doing something very wrong and you are about to override something in python built-in functionality.

Once again __ is also a convention to let people know you should not really touch this or modify this but dunder methods are most often used to define overloaded behaviors of predefined operators in Python.

For instance, arithmetic operators by default work upon numeric operands. This means that numeric objects must use along with operators like +, -, *, /, etc. The + operator is also defined as a concatenation operator in string, list, and tuple classes. We can say that the + operator is overloaded. In order to make the overloaded behaviour available in your own custom class, the corresponding magic method should be overridden.

__new__()

The __new__() magic method is implicitly called before the __init__() method. The __new__() method returns a new object, which is then initialized by __init__()

class Student:
    def __new__(cls):
        print ("__new__ magic method is called")
        temp = object.__new__(cls)
        return temp
    def __init__(self):
        print ("__init__ magic method is called")
        self.name='Satya'

student_obj1 = Student()

Output:

__new__ magic method is called
__init__ magic method is called

__str__()

str(12) built-in function returns '12'. When invoked, it calls the __str__() method in the int class.

print(str(12))            #Output: 12
print(int.__str__(num))   #Output: 12

Example:

class Student:
    def __init__(self):
        self.name='Michael'
    def __str__(self):
        return "Called __str__ dunder method"

s1=Student()
print(str(s1))          #Output: Called __str__ dunder method

print(s1)               #Output: Called __str__ dunder method

See how the str() function internally calls the __str__() method defined in the Student class. This is why it is called a magic method!

__add__()

For example, in order to use the + operator with objects of a user-defined class, it should include the __add__() method. For example,

class numbers:
    def __init__(self, x=None):
        self.num = x

    def __add__(self, x):
        temp = numbers()
        temp.result = self.num + x.num
        return temp

    def __str__(self):
        return f"Sum of numbers is {self.result}"

d1=numbers(3)
d2=numbers(3)

d3 = d1 + d2
print(d3)         #Output: Sum of numbers is 6

In the above example, the magic method __add__() is overridden, which performs the addition of the x attributes of the two objects. The __str__() method returns the object's string representation.

More reference from python docs

If you have any doubts or queries related to this chapter, get them clarified from our Python Team experts on ibmmainframer Community!