Python Encapsulation

Python encapsulation is focuses on data hiding and access control within classes. It enables the protection of an object’s attributes and methods, controlling how they are accessed and modified. Encapsulation allows you for the classification of attributes and methods as public, protected, or private. This concept enhances code organization, security, as it allows you for changes in the internal implementation of a class without affecting external code.

Imagine a scenario in which you are developing software for a banking system. In this system, customer accounts are represented as objects, and each account has a balance attribute that should be protected from unauthorized access and modification.

By applying encapsulation, you can mark balance attribute as private using a double underscore, like __balance, ensuring that it can only be accessed and modified through controlled methods provided by the class, such as deposit and withdrawal functions. This way, you prevent external code from directly manipulating a customer’s account balance, while encapsulating the implementation details within class.

Now that you have a fundamental grasp of Python encapsulation, let’s move forward and explore how this concept is put into practical use in real-life situations, illustrated through syntax.

Python Encapsulation Syntax

The Python encapsulation syntax is clear and simple to comprehend. This is how it looks:

Python encapsulation is primarily achieved through naming conventions rather than strict access modifiers. Here’s a summary of the common naming conventions to denote encapsulation levels:

I. Syntax of Public Members

The syntax for defining public members is provided below, and it is straightforward and easy to understand.

variable_name = value
def method_name(self):
      # Code here

Here, variable_name represents a variable that can hold a specific value. This value can be of various data types, such as numbers, text. On the other hand, method_name defines a function that can be used to perform specific actions within a class or object. The self parameter is used  to refer to instance of class, allowing you to access its attributes and methods.

Inside the method, you can write the code that carries out the desired operations or calculations using the provided value or any other data available within the class.

II. Syntax of Protected Members

The provided syntax for defining protected members is clear and easy to comprehend.

_variable_name = value
def _method_name(self):
    # Code here

In this syntax, when you want to mark members as protected by using a single underscore prefix, you just start the variable or method name with an underscore, like this: _method_name(self) for methods.

This underscores convention indicates that these members are meant to be considered as protected, implying that they should mainly be used internally within class or its subclasses.

III. Syntax of Private Members

The syntax given for declaring private members is simple and easily understood.

__variable_name = value
def __method_name(self):
     # Code here

This syntax closely resembles the one for protected members, where you start with _variable_name = value and define methods with code inside. However, for private members, you use a double underscore prefix (__variable_name = value) to indicate their privacy and restrict their access.

It’s important to note that Python doesn’t enforce strict encapsulation, and you can still access protected and private members if needed. The use of underscores serves as a naming convention and a signal to other developers about the intended level of visibility and usage.

I. Python Encapsulation

Now that you have a fundamental understanding of Python encapsulation and have explored its syntax from various angles, let’s dive into a basic example of encapsulation. This example will illustrate how encapsulation works in real-life scenarios.

Example Code
class Student: def __init__(self, name, roll_number): self._name = name self.__roll_number = roll_number def get_name(self): return self._name def set_name(self, name): self._name = name def get_roll_number(self): return self.__roll_number def set_roll_number(self, roll_number): self.__roll_number = roll_number student1 = Student("Harry", 101) print(f"Student name: {student1.get_name()}") student1.set_name("Wajjy") print(f"Modified student name: {student1.get_name()}") print(f"Student roll number: {student1.get_roll_number()}") student1.set_roll_number(102) print(f"Modified student roll number: {student1.get_roll_number()}")

For this example, we have defined a class called Student. Our class has two attributes: _name, which is designated as protected by starting it with a single underscore, and __roll_number, which is marked as private by using a double underscore prefix. We also provide getter and setter methods for both attributes, adhering to the principles of encapsulation. The getter methods, such as get_name() and get_roll_number(), allow us to access the attribute values, while the setter methods, like set_name() and set_roll_number(), provide a controlled way to modify these values.

We then create an instance of Student class named student1 with the name Harry and roll number 101. We access the protected attribute _name using the get_name() method, which returns Harry. Next, we use the set_name() method to change the student’s name to Wajjy and verify the modification by again calling get_name(), displaying the updated name.

Additionally, we access the private attribute __roll_number using get_roll_number() method, which initially returns 101. We then use set_roll_number() method to change the roll number to 102 and confirm the modification by once more calling get_roll_number(), which now returns 102.

Output
Student name: Harry
Modified student name: Wajjy
Student roll number: 101
Modified student roll number: 102

This above example ensures that you can interact with the class's internal data in a controlled and organized manner, maintaining data security.

II. Access Modifiers in Encapsulation

Access modifiers in encapsulation pertain to the keywords or conventions employed to ascertain how class members (which include attributes and methods) can be seen and utilized from beyond the class. In Python, there are three frequently employed access modifiers: now, let’s take a closer look at each of them:

A. Public Member

In Python, a public member is a class attribute or method that you can access and use from outside  class without any restrictions. These members are not limited by access control, so they are visible and can be used by your code outside  class without any constraints. Public members are defined without special prefixes or naming conventions, allowing you to freely retrieve and interact with them from any part of your program.

This simplicity makes it straightforward for you to work with data from other parts of your program. In encapsulation, public members do not require any specific access modifiers, and you can access them using the dot notation (e.g., object.attribute or object.method()). For example:

Example Code
class Book: def __init__(self, name, author, year): self.name = name self.author = author self.year = year def display_info(self): print(f"Book: {self.name}") print(f"Author: {self.author}") print(f"Year: {self.year}") book1 = Book("The Great Gatsby", "F. Scott Fitzgerald", 1925) print(f"Book name: {book1.name}") print(f"Author: {book1.author}") print(f"Publication year: {book1.year}")

Here, we crafted a Python class called Book that represents a book object. The class has an __init__ , which serves as the constructor and initializes the book's attributes: name, author, and year. These attributes store information about the book’s title, author, and publication year, respectively.

Additionally, there is a method within the class called display_info(), which prints out the details of the book, including its name, author, and publication year when called. To create an instance of  Book class, we instantiate it with the values for a specific book, We then use the dot notation to retrieve and display the attributes of book1, printing out its name, author, and publication year.

Output
Book name: The Great Gatsby
Author: F. Scott Fitzgerald
Publication year: 1925

As you can see, this example essentially illustrates the use of encapsulation by encapsulating the book's information within an object of the Book class, allowing you to easily access and display the book's details as needed.

B. Private Member

You can use a private member when you want to restrict direct retrieving and changing to class attributes or methods from outside class. Private members are indicated by a naming convention that involves adding a double underscore prefix to member’s name (e.g., __my_private_variable).

While this convention doesn’t make the member entirely private, it does mangle name by including the class name as a prefix, making it less likely to clash with similarly named attributes in subclasses or external code. Although it’s technically possible to access private members using their mangled names. For instance:

Example Code
class CityInfo: def __init__(self, city_name, food_specialty, population): self.__city_name = city_name self.__food_specialty = food_specialty self.__population = population def get_city_name(self): return self.__city_name def set_city_name(self, city_name): self.__city_name = city_name def get_food_specialty(self): return self.__food_specialty def set_food_specialty(self, food_specialty): self.__food_specialty = food_specialty def get_population(self): return self.__population def set_population(self, population): self.__population = population def display_info(self): print(f"City: {self.__city_name}") print(f"Food Specialty: {self.__food_specialty}") print(f"Population: {self.__population}") city_info = CityInfo("New York", "Pizza", 8400000) print(f"City name: {city_info.get_city_name()}") print(f"Food Specialty: {city_info.get_food_specialty()}") print(f"Population: {city_info.get_population()}")

In this example, we have collectively created a class named CityInfo to store information about cities, including their name, food specialty, and population. To ensure the privacy and controlled access of these attributes, we’ve used double underscores (e.g., __city_name) to make them private members of the class.

We have also thoughtfully implemented getter and setter methods for each private attribute. The getter methods like get_city_name(), get_food_specialty(), and get_population() allow us to retrieve the values of these private attributes, while the setter methods such as set_city_name(), set_food_specialty(), and set_population() enable us to modify these attributes in a controlled manner.

To visualize how this class works, we created an instance of CityInfo ,then by using the getter methods, we access and display the private attributes, ensuring that only approved methods can interact with and modify these sensitive city details.

Output
City name: New York
Food Specialty: Pizza
Population: 8400000

By using this approach, you can easily access the private members of CityInfo class while maintaining control over their modification. This encapsulation technique enhances data security and allows you to build robust and maintainable code by preventing unintended interference with sensitive attributes.

C. Protected Member

In the context of Python encapsulation, using protected access modifier empowers you to manage how class members (which encompass data and methods) are seen and reached. Protected members are restricted from external access but can be utilized and manipulated within both the class itself and its inheriting subclasses.

This access level strikes a balance between public and private access modifiers. It is a useful tool for maintaining controlled access within an inheritance hierarchy, enabling you to let subclasses utilize and override these members. Consider below illustration:

Example Code
class PrimeNumberGenerator: def __init__(self): self._primes = [2] def is_prime(self, num): if num < 2: return False for prime in self._primes: if num % prime == 0: return False return True def generate_primes_up_to(self, limit): for number in range(3, limit + 1): if self.is_prime(number): self._primes.append(number) def get_primes(self): return self._primes prime_generator = PrimeNumberGenerator() prime_generator.generate_primes_up_to(30) print("Prime numbers up to 30:", prime_generator.get_primes())

For this example, we defined PrimeNumberGenerator class to work with prime numbers. Within this class, we’ve defined several methods to handle prime number generation and checking. We’ve initialized a protected member called _primes with the value [2] in the class constructor, starting with the first prime number.

To evaluate if a given number is prime, we’ve implemented is_prime method. This method checks if the input number is less than 2 (non-prime) and then iterates through _primes list to see if the number is divisible by any previously found prime numbers. If it’s divisible, the method returns False; otherwise, it returns True.

The generate_primes_up_to method generates prime numbers up to a specified limit. It starts iterating from 3 up to the given limit and uses is_prime method to check if each number is prime. If a number is prime, it’s added to  _primes list. Finally, we’ve included a get_primes method to allow external code to access the list of prime numbers stored in the _primes member. In the end, we create an instance of PrimeNumberGenerator class, prime_generator, and use it to generate prime numbers up to 30. We then print out the prime numbers found up to that limit using the get_primes method.

Output
Prime numbers up to 30: [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]

By using this approach, you can see how encapsulation is employed to safeguard the _primes member within the PrimeNumberGenerator class. This ensures that the _primes member is handled and retrieved exclusively through the specified methods within the class.

Python Encapsulation Advanced Examples

Now that you’ve developed a solid grasp of Python encapsulation and have explored them in various scenarios, let’s examine some advanced examples of Python encapsulation. This exploration will provide you with a clearer picture of this concept, which holds significant value in object-oriented programming.

I. Getters and Setters in Encapsulation

The getters and setters work as accessors and mutators for your attributes, giving you ability to manage how external code interacts with and modifies the data inside an object. Getters, typically named with a get prefix, allow you to retrieve attribute values while keeping their internal representation hidden. Setters, named with a set prefix, provide a controlled way to modify attribute values, often including validation and logic checks before applying changes.

By employing this method, you enhance the manageability of your code through the enforcement of regulated access to object attributes and establishment of a uniform interface for external code to engage with your objects. This, in turn, aids in achieving data encapsulation and abstraction more efficiently. For example:

Example Code
class FibonacciGenerator: def __init__(self): self._fib_sequence = [0, 1] def get_nth_fibonacci(self, n): if n < 0: return None elif n < len(self._fib_sequence): return self._fib_sequence[n] else: while len(self._fib_sequence) <= n: next_fib = self._fib_sequence[-1] + self._fib_sequence[-2] self._fib_sequence.append(next_fib) return self._fib_sequence[n] def set_nth_fibonacci(self, n, value): if n < 0: return False elif n < len(self._fib_sequence): self._fib_sequence[n] = value else: return False def get_sequence(self): return self._fib_sequence # Example usage: fibonacci_generator = FibonacciGenerator() print("Fibonacci sequence:", fibonacci_generator.get_sequence()) n = 10 print(f"The {n}-th Fibonacci number is:", fibonacci_generator.get_nth_fibonacci(n)) n_to_set = 5 new_value = 42 if fibonacci_generator.set_nth_fibonacci(n_to_set, new_value): print(f"Updated {n_to_set}-th Fibonacci number to {new_value}") else: print(f"Failed to set {n_to_set}-th Fibonacci number. Sequence length insufficient.") print("Updated Fibonacci sequence:", fibonacci_generator.get_sequence())

Here, Initially, we initialize the _fib_sequence attribute with the first two Fibonacci numbers, 0 and 1, in the class constructor. The get_nth_fibonacci method acts as a getter, allowing us to retrieve the nth Fibonacci number from the sequence. It first checks for valid input (non-negative integer) and then either returns the value if it’s already calculated or computes it by extending the sequence.

On the other hand, the set_nth_fibonacci method serves as a setter, enabling us to modify the value at nth position in the sequence. It also validates the input to ensure it’s non-negative and within the current sequence length. We maintain the Fibonacci sequence encapsulated within  class, ensuring controlled access to it.

In the end, we create an instance of FibonacciGenerator class, fibonacci_generator, and showcase its capabilities. We retrieve the entire Fibonacci sequence using get_sequence method and access the 10th Fibonacci number using get_nth_fibonacci. We also attempt to modify the 5th Fibonacci number and print the updated sequence, highlighting how encapsulation helps maintain data integrity and abstraction while allowing controlled interaction with the sequence.

Output
Fibonacci sequence: [0, 1]
The 10-th Fibonacci number is: 55
Failed to set 5-th Fibonacci number. Sequence length insufficient.
Updated Fibonacci sequence: [0, 1, 1, 2, 3, 42, 8, 13, 21, 34, 55]

In summary, the above example illustrates encapsulation through getters and setters in a class that manages the Fibonacci sequence, ensuring controlled access and modification while maintaining data integrity.

II. Exception Handling with Encapsulation

Exception handling with Python encapsulation provides you with a structured approach to manage errors and unexpected situations within your code while preserving the integrity and abstraction of your data through encapsulation principles. By encapsulating your data and providing controlled access through methods like getters and setters, you establish clear boundaries for external code.

When exceptions occur, you can catch them within the class, implementing custom error handling logic specific to the class’s context. This ensures that the internal state of your objects remains consistent, preventing unintended access or modification of data. For example:

Example Code
class BankAccount: def __init__(self, balance=0): if balance < 0: raise ValueError("Initial balance cannot be negative") self._balance = balance def get_balance(self): return self._balance def deposit(self, amount): if amount <= 0: raise ValueError("Deposit amount must be greater than zero") self._balance += amount def withdraw(self, amount): if amount <= 0: raise ValueError("Withdrawal amount must be greater than zero") if amount > self._balance: raise ValueError("Insufficient balance for withdrawal") self._balance -= amount try: account = BankAccount(-100) except ValueError as e: print("Error:", e) account = BankAccount(1000) try: account.deposit(-200) except ValueError as e: print("Error:", e) try: account.withdraw(1500) except ValueError as e: print("Error:", e) print("Current balance:", account.get_balance())

In this example, The BankAccount class encapsulates the concept of a bank account, allowing you to set the initial balance during account creation. We’ve used encapsulation by keeping the balance attribute as a protected member (_balance) and controlling access through methods. In constructor (__init__), we’ve implemented error handling to ensure that the initial balance can’t be negative. If a negative balance is detected, it triggers a ValueError exception, preventing the creation of an account with an invalid balance.

Additionally, the deposit and withdraw methods, which enable balance modifications, include checks to ensure that deposit and withdrawal amounts are positive and that there are sufficient funds for withdrawals. If any of these conditions are not met, a ValueError exception is raised to indicate an error.

Next, we attempt to create an account with a negative initial balance, make a negative deposit, and withdraw more money than the account holds. In each case, the code raises a ValueError exception, and we use error handling to capture and handle these exceptions gracefully by providing informative error messages. Finally, we verify that the account balance remains unchanged after these exceptions.

Output
Error: Initial balance cannot be negative
Error: Deposit amount must be greater than zero
Error: Insufficient balance for withdrawal
Current balance: 1000

As you can see, this approach illustrates how encapsulation and well-structured exception handling work together to ensure data integrity, protect sensitive attributes, and handle errors gracefully, ultimately enhancing the reliability and maintainability of your code.

Now that you have gained a firm grasp of Python encapsulation and have explored them in various scenarios, let’s delve into the theoretical aspects of encapsulation. Understanding these theoretical concepts is crucial in programming as they play a significant role in shaping your coding practices and overall programming knowledge.

Advantages of Encapsulation

Certainly, here are the advantages of encapsulation in python:

I. Data Protection

Python Encapsulation shields your data from unauthorized access and modification, preserving its integrity.

II. Abstraction

It hides the underlying implementation details, allowing you to work with objects at a higher level of abstraction, simplifying complex systems.

III. Controlled Access

You can control how external code interacts with your objects, reducing the risk of unintended data changes.

IV. Flexibility

By providing methods to access and modify data, you can change the internal implementation of a class without affecting the code that uses it.

V. Code Organization

Encapsulation promotes better organization of code by grouping related data and behavior into classes.

Congratulations on delving into the realm of Python encapsulation! You’ve just unlocked an amazing technique that’s all about safeguarding your data and controlling how it’s accessed and modified within classes. With encapsulation, you can classify attributes and methods, giving you fine-grained control over who can interact with them. This concept brings a host of benefits, including better code organization, heightened security, and flexibility in changing the internal workings of your classes without disrupting external code.

In this Python Helper guide, you’ve delved into the flexible and convenient uses of Python encapsulation across various scenarios. Initially, you’ve examined the naming conventions associated with encapsulation, which encompass public, protected, and private attributes. Later, you’ve ventured into more advanced applications, including the use of getters and setters, as well as gaining insights into handling exceptions within encapsulated code structures.

By mastering these encapsulation techniques, you’ll become a more skilled and confident Python programmer, capable of building robust and maintainable code. Keep up the great work!

 
Scroll to Top