Chapter 8: Object-Oriented Programming
8.5: Encapsulation
Encapsulation is one of the most fundamental principles of object-oriented programming. It is the process of combining data (attributes) and methods that operate on that data within a single unit, usually a class, to create a cohesive and well-organized system.
By using encapsulation, we can limit access to certain parts of the object, which can help prevent unwanted interference or modification of the object's internal state. This can be especially useful in large and complex systems, where keeping track of the state of different objects can become difficult. Additionally, encapsulation can make it easier to modify and update the code, as changes to one part of the code will have less of an impact on the rest of the system. Overall, encapsulation is a powerful technique that can help create more robust and maintainable code.
Encapsulation is achieved by using private and protected access specifiers for attributes and methods. In Python, there is no strict concept of private or protected members, but we follow certain conventions to indicate the intended access level:
8.5.1: Public members:
By default, all members of a class are public, meaning they can be accessed from anywhere inside and outside the class. However, it is important to note that making all members public can lead to potential security issues, as sensitive information may be accessed or modified by unauthorized users.
To mitigate this risk, it is recommended to use access modifiers such as private or protected for sensitive members, and only provide public access to necessary members. Additionally, using encapsulation techniques such as getters and setters can help ensure that data is accessed and modified in a controlled and secure manner.
8.5.2: Protected members:
If a member is intended to be accessed only from within the class and its subclasses, its name should be prefixed with a single underscore (_). This is known as a convention and it is widely used in Python. However, it is important to note that this convention does not actually prevent access to the member from outside the class or its subclasses.
In such cases, it is recommended to use name mangling, a technique that adds a prefix to the name of the member to make it harder to access from outside the class. Name mangling is achieved by prefixing the member name with two underscores (__) and a suffix of one or more underscores. For example, a member named "my_var" would become "_MyClass__my_var" in the class called "MyClass". Note that this technique should be used with caution, as it can make the code harder to read and maintain.
8.5.3: Private members:
If a member is intended to be accessed only within the class (not even by subclasses), its name should be prefixed with double underscores (__). Python does provide a limited form of privacy by name mangling, which makes it difficult but not impossible to access the member from outside the class.
It is important to understand that name mangling is not a form of security. It is simply a convention used to discourage accidental access to private members. In fact, name mangling can be easily circumvented by accessing the member using its mangled name.
In addition, Python also allows for protected members, which can be accessed by subclasses but not from outside the class. These members are prefixed with a single underscore (_).
It is worth noting that the use of private and protected members is not necessary in all cases. In many cases, it is perfectly acceptable to make all members public. However, in larger projects or projects with multiple developers, the use of private and protected members can help to prevent unintended modifications to critical parts of the code.
Example:
class BankAccount:
    def __init__(self, account_number, balance):
        self._account_number = account_number
        self.__balance = balance
    def deposit(self, amount):
        self.__balance += amount
    def withdraw(self, amount):
        if amount <= self.__balance:
            self.__balance -= amount
        else:
            print("Insufficient funds")
    def get_balance(self):
        return self.__balance
account = BankAccount("12345", 1000)
account.deposit(500)
account.withdraw(300)
print(account.get_balance())  # Output: 1200In this example, the BankAccount class has an attribute __balance, which is intended to be private. We have provided methods like deposit(), withdraw(), and get_balance() to manipulate the balance, thus preventing direct access to the attribute. Note that _account_number is a protected member, which is not enforced by Python but signifies that it should be treated as protected by convention.
Exercise 8.5.1: Create a Simple Employee Class
In this exercise, you'll create a simple Employee class that uses encapsulation to protect its data members.
Instructions:
- Create a class called Employee.
- Define the following private attributes: __first_name,__last_name, and__salary.
- Create a constructor that takes first_name,last_name, andsalaryas parameters and initializes the private attributes.
- Create public methods get_first_name(),get_last_name(), andget_salary()that return the respective attributes.
- Create a method get_full_name()that returns the employee's full name, which is the combination of the first and last name.
Solution:
class Employee:
    def __init__(self, first_name, last_name, salary):
        self.__first_name = first_name
        self.__last_name = last_name
        self.__salary = salary
    def get_first_name(self):
        return self.__first_name
    def get_last_name(self):
        return self.__last_name
    def get_salary(self):
        return self.__salary
    def get_full_name(self):
        return self.__first_name + " " + self.__last_name
employee = Employee("John", "Doe", 50000)
print(employee.get_full_name())  # Output: John DoeExercise 8.5.2: Implementing a Circle Class
In this exercise, you'll create a Circle class that uses encapsulation to protect its data members and provide methods to manipulate them.
Instructions:
- Create a class called Circle.
- Define the following private attributes: __radiusand__pi(use the value 3.14159 for pi).
- Create a constructor that takes radiusas a parameter and initializes the private attribute__radius.
- Create public methods get_radius()andget_pi()that return the respective attributes.
- Create methods calculate_area()andcalculate_circumference()that return the area and circumference of the circle, respectively.
Solution:
class Circle:
    def __init__(self, radius):
        self.__radius = radius
        self.__pi = 3.14159
    def get_radius(self):
        return self.__radius
    def get_pi(self):
        return self.__pi
    def calculate_area(self):
        return self.__pi * self.__radius ** 2
    def calculate_circumference(self):
        return 2 * self.__pi * self.__radius
circle = Circle(5)
print(circle.calculate_area())         # Output: 78.53975
print(circle.calculate_circumference())  # Output: 31.4159Exercise 8.5.3: Creating a Password Protected Account
In this exercise, you'll create an Account class that uses encapsulation to protect its data members and requires a password to access certain methods.
Instructions:
- Create a class called Account.
- Define the following private attributes: __account_number,__balance, and__password.
- Create a constructor that takes account_number,balance, andpasswordas parameters and initializes the private attributes.
- Create public methods get_account_number()andget_balance()that return the respective attributes.
- Create a method validate_password(self, password)that returnsTrueif the given password matches the account's password, andFalseotherwise.
- Create a method withdraw(self, amount, password)that checks if the password is correct usingvalidate_password(), and if so, subtracts the given amount from the balance.
Solution:
class Account:
    def __init__(self, account_number, balance, password):
        self.__account_number = account_number
        self.__balance = balance
        self.__password = password
    def get_account_number(self):
        return self.__account_number
    def get_balance(self):
        return self.__balance
    def validate_password(self, password):
        return self.__password == password
    def withdraw(self, amount, password):
        if self.validate_password(password):
            self.__balance -= amount
            return True
        return False
account = Account("123456", 1000, "secret123")
print(account.get_account_number())  # Output: 123456
print(account.get_balance())         # Output: 1000
if account.withdraw(200, "secret123"):
    print("Withdrawal successful!")
    print("New balance:", account.get_balance())  # Output: 800
else:
    print("Incorrect password or insufficient funds.")In this exercise, you've created an Account class that demonstrates the concept of encapsulation by protecting its data members and requiring a password for certain operations.
As we conclude Chapter 8, it's important to recognize the significance of Object-Oriented Programming (OOP) in Python. Through OOP, we can create more organized, maintainable, and scalable code. In this chapter, we have explored the key OOP concepts:
- Classes and Objects: The fundamentals of creating custom data types and instances in Python.
- Attributes and Methods: How to store data and define behaviors within classes.
- Inheritance: A way to create new classes from existing ones, promoting code reusability.
- Polymorphism: Leveraging the power of inheritance and method overriding to create flexible code that can handle different types of objects.
- Encapsulation: Protecting the internal state and implementation of a class, providing a well-defined interface for interacting with it.
By applying these principles in your Python programs, you can create code that is easier to understand, debug, and extend. Don't forget to practice implementing these concepts through exercises and real-world projects to gain a deeper understanding of OOP in Python.
As you move forward, remember that Python is a versatile language that supports multiple programming paradigms. Combining OOP with other approaches, such as functional programming, can help you further refine and tailor your code to suit various situations and requirements. See you in the next chapter. Happy coding!
8.5: Encapsulation
Encapsulation is one of the most fundamental principles of object-oriented programming. It is the process of combining data (attributes) and methods that operate on that data within a single unit, usually a class, to create a cohesive and well-organized system.
By using encapsulation, we can limit access to certain parts of the object, which can help prevent unwanted interference or modification of the object's internal state. This can be especially useful in large and complex systems, where keeping track of the state of different objects can become difficult. Additionally, encapsulation can make it easier to modify and update the code, as changes to one part of the code will have less of an impact on the rest of the system. Overall, encapsulation is a powerful technique that can help create more robust and maintainable code.
Encapsulation is achieved by using private and protected access specifiers for attributes and methods. In Python, there is no strict concept of private or protected members, but we follow certain conventions to indicate the intended access level:
8.5.1: Public members:
By default, all members of a class are public, meaning they can be accessed from anywhere inside and outside the class. However, it is important to note that making all members public can lead to potential security issues, as sensitive information may be accessed or modified by unauthorized users.
To mitigate this risk, it is recommended to use access modifiers such as private or protected for sensitive members, and only provide public access to necessary members. Additionally, using encapsulation techniques such as getters and setters can help ensure that data is accessed and modified in a controlled and secure manner.
8.5.2: Protected members:
If a member is intended to be accessed only from within the class and its subclasses, its name should be prefixed with a single underscore (_). This is known as a convention and it is widely used in Python. However, it is important to note that this convention does not actually prevent access to the member from outside the class or its subclasses.
In such cases, it is recommended to use name mangling, a technique that adds a prefix to the name of the member to make it harder to access from outside the class. Name mangling is achieved by prefixing the member name with two underscores (__) and a suffix of one or more underscores. For example, a member named "my_var" would become "_MyClass__my_var" in the class called "MyClass". Note that this technique should be used with caution, as it can make the code harder to read and maintain.
8.5.3: Private members:
If a member is intended to be accessed only within the class (not even by subclasses), its name should be prefixed with double underscores (__). Python does provide a limited form of privacy by name mangling, which makes it difficult but not impossible to access the member from outside the class.
It is important to understand that name mangling is not a form of security. It is simply a convention used to discourage accidental access to private members. In fact, name mangling can be easily circumvented by accessing the member using its mangled name.
In addition, Python also allows for protected members, which can be accessed by subclasses but not from outside the class. These members are prefixed with a single underscore (_).
It is worth noting that the use of private and protected members is not necessary in all cases. In many cases, it is perfectly acceptable to make all members public. However, in larger projects or projects with multiple developers, the use of private and protected members can help to prevent unintended modifications to critical parts of the code.
Example:
class BankAccount:
    def __init__(self, account_number, balance):
        self._account_number = account_number
        self.__balance = balance
    def deposit(self, amount):
        self.__balance += amount
    def withdraw(self, amount):
        if amount <= self.__balance:
            self.__balance -= amount
        else:
            print("Insufficient funds")
    def get_balance(self):
        return self.__balance
account = BankAccount("12345", 1000)
account.deposit(500)
account.withdraw(300)
print(account.get_balance())  # Output: 1200In this example, the BankAccount class has an attribute __balance, which is intended to be private. We have provided methods like deposit(), withdraw(), and get_balance() to manipulate the balance, thus preventing direct access to the attribute. Note that _account_number is a protected member, which is not enforced by Python but signifies that it should be treated as protected by convention.
Exercise 8.5.1: Create a Simple Employee Class
In this exercise, you'll create a simple Employee class that uses encapsulation to protect its data members.
Instructions:
- Create a class called Employee.
- Define the following private attributes: __first_name,__last_name, and__salary.
- Create a constructor that takes first_name,last_name, andsalaryas parameters and initializes the private attributes.
- Create public methods get_first_name(),get_last_name(), andget_salary()that return the respective attributes.
- Create a method get_full_name()that returns the employee's full name, which is the combination of the first and last name.
Solution:
class Employee:
    def __init__(self, first_name, last_name, salary):
        self.__first_name = first_name
        self.__last_name = last_name
        self.__salary = salary
    def get_first_name(self):
        return self.__first_name
    def get_last_name(self):
        return self.__last_name
    def get_salary(self):
        return self.__salary
    def get_full_name(self):
        return self.__first_name + " " + self.__last_name
employee = Employee("John", "Doe", 50000)
print(employee.get_full_name())  # Output: John DoeExercise 8.5.2: Implementing a Circle Class
In this exercise, you'll create a Circle class that uses encapsulation to protect its data members and provide methods to manipulate them.
Instructions:
- Create a class called Circle.
- Define the following private attributes: __radiusand__pi(use the value 3.14159 for pi).
- Create a constructor that takes radiusas a parameter and initializes the private attribute__radius.
- Create public methods get_radius()andget_pi()that return the respective attributes.
- Create methods calculate_area()andcalculate_circumference()that return the area and circumference of the circle, respectively.
Solution:
class Circle:
    def __init__(self, radius):
        self.__radius = radius
        self.__pi = 3.14159
    def get_radius(self):
        return self.__radius
    def get_pi(self):
        return self.__pi
    def calculate_area(self):
        return self.__pi * self.__radius ** 2
    def calculate_circumference(self):
        return 2 * self.__pi * self.__radius
circle = Circle(5)
print(circle.calculate_area())         # Output: 78.53975
print(circle.calculate_circumference())  # Output: 31.4159Exercise 8.5.3: Creating a Password Protected Account
In this exercise, you'll create an Account class that uses encapsulation to protect its data members and requires a password to access certain methods.
Instructions:
- Create a class called Account.
- Define the following private attributes: __account_number,__balance, and__password.
- Create a constructor that takes account_number,balance, andpasswordas parameters and initializes the private attributes.
- Create public methods get_account_number()andget_balance()that return the respective attributes.
- Create a method validate_password(self, password)that returnsTrueif the given password matches the account's password, andFalseotherwise.
- Create a method withdraw(self, amount, password)that checks if the password is correct usingvalidate_password(), and if so, subtracts the given amount from the balance.
Solution:
class Account:
    def __init__(self, account_number, balance, password):
        self.__account_number = account_number
        self.__balance = balance
        self.__password = password
    def get_account_number(self):
        return self.__account_number
    def get_balance(self):
        return self.__balance
    def validate_password(self, password):
        return self.__password == password
    def withdraw(self, amount, password):
        if self.validate_password(password):
            self.__balance -= amount
            return True
        return False
account = Account("123456", 1000, "secret123")
print(account.get_account_number())  # Output: 123456
print(account.get_balance())         # Output: 1000
if account.withdraw(200, "secret123"):
    print("Withdrawal successful!")
    print("New balance:", account.get_balance())  # Output: 800
else:
    print("Incorrect password or insufficient funds.")In this exercise, you've created an Account class that demonstrates the concept of encapsulation by protecting its data members and requiring a password for certain operations.
As we conclude Chapter 8, it's important to recognize the significance of Object-Oriented Programming (OOP) in Python. Through OOP, we can create more organized, maintainable, and scalable code. In this chapter, we have explored the key OOP concepts:
- Classes and Objects: The fundamentals of creating custom data types and instances in Python.
- Attributes and Methods: How to store data and define behaviors within classes.
- Inheritance: A way to create new classes from existing ones, promoting code reusability.
- Polymorphism: Leveraging the power of inheritance and method overriding to create flexible code that can handle different types of objects.
- Encapsulation: Protecting the internal state and implementation of a class, providing a well-defined interface for interacting with it.
By applying these principles in your Python programs, you can create code that is easier to understand, debug, and extend. Don't forget to practice implementing these concepts through exercises and real-world projects to gain a deeper understanding of OOP in Python.
As you move forward, remember that Python is a versatile language that supports multiple programming paradigms. Combining OOP with other approaches, such as functional programming, can help you further refine and tailor your code to suit various situations and requirements. See you in the next chapter. Happy coding!
8.5: Encapsulation
Encapsulation is one of the most fundamental principles of object-oriented programming. It is the process of combining data (attributes) and methods that operate on that data within a single unit, usually a class, to create a cohesive and well-organized system.
By using encapsulation, we can limit access to certain parts of the object, which can help prevent unwanted interference or modification of the object's internal state. This can be especially useful in large and complex systems, where keeping track of the state of different objects can become difficult. Additionally, encapsulation can make it easier to modify and update the code, as changes to one part of the code will have less of an impact on the rest of the system. Overall, encapsulation is a powerful technique that can help create more robust and maintainable code.
Encapsulation is achieved by using private and protected access specifiers for attributes and methods. In Python, there is no strict concept of private or protected members, but we follow certain conventions to indicate the intended access level:
8.5.1: Public members:
By default, all members of a class are public, meaning they can be accessed from anywhere inside and outside the class. However, it is important to note that making all members public can lead to potential security issues, as sensitive information may be accessed or modified by unauthorized users.
To mitigate this risk, it is recommended to use access modifiers such as private or protected for sensitive members, and only provide public access to necessary members. Additionally, using encapsulation techniques such as getters and setters can help ensure that data is accessed and modified in a controlled and secure manner.
8.5.2: Protected members:
If a member is intended to be accessed only from within the class and its subclasses, its name should be prefixed with a single underscore (_). This is known as a convention and it is widely used in Python. However, it is important to note that this convention does not actually prevent access to the member from outside the class or its subclasses.
In such cases, it is recommended to use name mangling, a technique that adds a prefix to the name of the member to make it harder to access from outside the class. Name mangling is achieved by prefixing the member name with two underscores (__) and a suffix of one or more underscores. For example, a member named "my_var" would become "_MyClass__my_var" in the class called "MyClass". Note that this technique should be used with caution, as it can make the code harder to read and maintain.
8.5.3: Private members:
If a member is intended to be accessed only within the class (not even by subclasses), its name should be prefixed with double underscores (__). Python does provide a limited form of privacy by name mangling, which makes it difficult but not impossible to access the member from outside the class.
It is important to understand that name mangling is not a form of security. It is simply a convention used to discourage accidental access to private members. In fact, name mangling can be easily circumvented by accessing the member using its mangled name.
In addition, Python also allows for protected members, which can be accessed by subclasses but not from outside the class. These members are prefixed with a single underscore (_).
It is worth noting that the use of private and protected members is not necessary in all cases. In many cases, it is perfectly acceptable to make all members public. However, in larger projects or projects with multiple developers, the use of private and protected members can help to prevent unintended modifications to critical parts of the code.
Example:
class BankAccount:
    def __init__(self, account_number, balance):
        self._account_number = account_number
        self.__balance = balance
    def deposit(self, amount):
        self.__balance += amount
    def withdraw(self, amount):
        if amount <= self.__balance:
            self.__balance -= amount
        else:
            print("Insufficient funds")
    def get_balance(self):
        return self.__balance
account = BankAccount("12345", 1000)
account.deposit(500)
account.withdraw(300)
print(account.get_balance())  # Output: 1200In this example, the BankAccount class has an attribute __balance, which is intended to be private. We have provided methods like deposit(), withdraw(), and get_balance() to manipulate the balance, thus preventing direct access to the attribute. Note that _account_number is a protected member, which is not enforced by Python but signifies that it should be treated as protected by convention.
Exercise 8.5.1: Create a Simple Employee Class
In this exercise, you'll create a simple Employee class that uses encapsulation to protect its data members.
Instructions:
- Create a class called Employee.
- Define the following private attributes: __first_name,__last_name, and__salary.
- Create a constructor that takes first_name,last_name, andsalaryas parameters and initializes the private attributes.
- Create public methods get_first_name(),get_last_name(), andget_salary()that return the respective attributes.
- Create a method get_full_name()that returns the employee's full name, which is the combination of the first and last name.
Solution:
class Employee:
    def __init__(self, first_name, last_name, salary):
        self.__first_name = first_name
        self.__last_name = last_name
        self.__salary = salary
    def get_first_name(self):
        return self.__first_name
    def get_last_name(self):
        return self.__last_name
    def get_salary(self):
        return self.__salary
    def get_full_name(self):
        return self.__first_name + " " + self.__last_name
employee = Employee("John", "Doe", 50000)
print(employee.get_full_name())  # Output: John DoeExercise 8.5.2: Implementing a Circle Class
In this exercise, you'll create a Circle class that uses encapsulation to protect its data members and provide methods to manipulate them.
Instructions:
- Create a class called Circle.
- Define the following private attributes: __radiusand__pi(use the value 3.14159 for pi).
- Create a constructor that takes radiusas a parameter and initializes the private attribute__radius.
- Create public methods get_radius()andget_pi()that return the respective attributes.
- Create methods calculate_area()andcalculate_circumference()that return the area and circumference of the circle, respectively.
Solution:
class Circle:
    def __init__(self, radius):
        self.__radius = radius
        self.__pi = 3.14159
    def get_radius(self):
        return self.__radius
    def get_pi(self):
        return self.__pi
    def calculate_area(self):
        return self.__pi * self.__radius ** 2
    def calculate_circumference(self):
        return 2 * self.__pi * self.__radius
circle = Circle(5)
print(circle.calculate_area())         # Output: 78.53975
print(circle.calculate_circumference())  # Output: 31.4159Exercise 8.5.3: Creating a Password Protected Account
In this exercise, you'll create an Account class that uses encapsulation to protect its data members and requires a password to access certain methods.
Instructions:
- Create a class called Account.
- Define the following private attributes: __account_number,__balance, and__password.
- Create a constructor that takes account_number,balance, andpasswordas parameters and initializes the private attributes.
- Create public methods get_account_number()andget_balance()that return the respective attributes.
- Create a method validate_password(self, password)that returnsTrueif the given password matches the account's password, andFalseotherwise.
- Create a method withdraw(self, amount, password)that checks if the password is correct usingvalidate_password(), and if so, subtracts the given amount from the balance.
Solution:
class Account:
    def __init__(self, account_number, balance, password):
        self.__account_number = account_number
        self.__balance = balance
        self.__password = password
    def get_account_number(self):
        return self.__account_number
    def get_balance(self):
        return self.__balance
    def validate_password(self, password):
        return self.__password == password
    def withdraw(self, amount, password):
        if self.validate_password(password):
            self.__balance -= amount
            return True
        return False
account = Account("123456", 1000, "secret123")
print(account.get_account_number())  # Output: 123456
print(account.get_balance())         # Output: 1000
if account.withdraw(200, "secret123"):
    print("Withdrawal successful!")
    print("New balance:", account.get_balance())  # Output: 800
else:
    print("Incorrect password or insufficient funds.")In this exercise, you've created an Account class that demonstrates the concept of encapsulation by protecting its data members and requiring a password for certain operations.
As we conclude Chapter 8, it's important to recognize the significance of Object-Oriented Programming (OOP) in Python. Through OOP, we can create more organized, maintainable, and scalable code. In this chapter, we have explored the key OOP concepts:
- Classes and Objects: The fundamentals of creating custom data types and instances in Python.
- Attributes and Methods: How to store data and define behaviors within classes.
- Inheritance: A way to create new classes from existing ones, promoting code reusability.
- Polymorphism: Leveraging the power of inheritance and method overriding to create flexible code that can handle different types of objects.
- Encapsulation: Protecting the internal state and implementation of a class, providing a well-defined interface for interacting with it.
By applying these principles in your Python programs, you can create code that is easier to understand, debug, and extend. Don't forget to practice implementing these concepts through exercises and real-world projects to gain a deeper understanding of OOP in Python.
As you move forward, remember that Python is a versatile language that supports multiple programming paradigms. Combining OOP with other approaches, such as functional programming, can help you further refine and tailor your code to suit various situations and requirements. See you in the next chapter. Happy coding!
8.5: Encapsulation
Encapsulation is one of the most fundamental principles of object-oriented programming. It is the process of combining data (attributes) and methods that operate on that data within a single unit, usually a class, to create a cohesive and well-organized system.
By using encapsulation, we can limit access to certain parts of the object, which can help prevent unwanted interference or modification of the object's internal state. This can be especially useful in large and complex systems, where keeping track of the state of different objects can become difficult. Additionally, encapsulation can make it easier to modify and update the code, as changes to one part of the code will have less of an impact on the rest of the system. Overall, encapsulation is a powerful technique that can help create more robust and maintainable code.
Encapsulation is achieved by using private and protected access specifiers for attributes and methods. In Python, there is no strict concept of private or protected members, but we follow certain conventions to indicate the intended access level:
8.5.1: Public members:
By default, all members of a class are public, meaning they can be accessed from anywhere inside and outside the class. However, it is important to note that making all members public can lead to potential security issues, as sensitive information may be accessed or modified by unauthorized users.
To mitigate this risk, it is recommended to use access modifiers such as private or protected for sensitive members, and only provide public access to necessary members. Additionally, using encapsulation techniques such as getters and setters can help ensure that data is accessed and modified in a controlled and secure manner.
8.5.2: Protected members:
If a member is intended to be accessed only from within the class and its subclasses, its name should be prefixed with a single underscore (_). This is known as a convention and it is widely used in Python. However, it is important to note that this convention does not actually prevent access to the member from outside the class or its subclasses.
In such cases, it is recommended to use name mangling, a technique that adds a prefix to the name of the member to make it harder to access from outside the class. Name mangling is achieved by prefixing the member name with two underscores (__) and a suffix of one or more underscores. For example, a member named "my_var" would become "_MyClass__my_var" in the class called "MyClass". Note that this technique should be used with caution, as it can make the code harder to read and maintain.
8.5.3: Private members:
If a member is intended to be accessed only within the class (not even by subclasses), its name should be prefixed with double underscores (__). Python does provide a limited form of privacy by name mangling, which makes it difficult but not impossible to access the member from outside the class.
It is important to understand that name mangling is not a form of security. It is simply a convention used to discourage accidental access to private members. In fact, name mangling can be easily circumvented by accessing the member using its mangled name.
In addition, Python also allows for protected members, which can be accessed by subclasses but not from outside the class. These members are prefixed with a single underscore (_).
It is worth noting that the use of private and protected members is not necessary in all cases. In many cases, it is perfectly acceptable to make all members public. However, in larger projects or projects with multiple developers, the use of private and protected members can help to prevent unintended modifications to critical parts of the code.
Example:
class BankAccount:
    def __init__(self, account_number, balance):
        self._account_number = account_number
        self.__balance = balance
    def deposit(self, amount):
        self.__balance += amount
    def withdraw(self, amount):
        if amount <= self.__balance:
            self.__balance -= amount
        else:
            print("Insufficient funds")
    def get_balance(self):
        return self.__balance
account = BankAccount("12345", 1000)
account.deposit(500)
account.withdraw(300)
print(account.get_balance())  # Output: 1200In this example, the BankAccount class has an attribute __balance, which is intended to be private. We have provided methods like deposit(), withdraw(), and get_balance() to manipulate the balance, thus preventing direct access to the attribute. Note that _account_number is a protected member, which is not enforced by Python but signifies that it should be treated as protected by convention.
Exercise 8.5.1: Create a Simple Employee Class
In this exercise, you'll create a simple Employee class that uses encapsulation to protect its data members.
Instructions:
- Create a class called Employee.
- Define the following private attributes: __first_name,__last_name, and__salary.
- Create a constructor that takes first_name,last_name, andsalaryas parameters and initializes the private attributes.
- Create public methods get_first_name(),get_last_name(), andget_salary()that return the respective attributes.
- Create a method get_full_name()that returns the employee's full name, which is the combination of the first and last name.
Solution:
class Employee:
    def __init__(self, first_name, last_name, salary):
        self.__first_name = first_name
        self.__last_name = last_name
        self.__salary = salary
    def get_first_name(self):
        return self.__first_name
    def get_last_name(self):
        return self.__last_name
    def get_salary(self):
        return self.__salary
    def get_full_name(self):
        return self.__first_name + " " + self.__last_name
employee = Employee("John", "Doe", 50000)
print(employee.get_full_name())  # Output: John DoeExercise 8.5.2: Implementing a Circle Class
In this exercise, you'll create a Circle class that uses encapsulation to protect its data members and provide methods to manipulate them.
Instructions:
- Create a class called Circle.
- Define the following private attributes: __radiusand__pi(use the value 3.14159 for pi).
- Create a constructor that takes radiusas a parameter and initializes the private attribute__radius.
- Create public methods get_radius()andget_pi()that return the respective attributes.
- Create methods calculate_area()andcalculate_circumference()that return the area and circumference of the circle, respectively.
Solution:
class Circle:
    def __init__(self, radius):
        self.__radius = radius
        self.__pi = 3.14159
    def get_radius(self):
        return self.__radius
    def get_pi(self):
        return self.__pi
    def calculate_area(self):
        return self.__pi * self.__radius ** 2
    def calculate_circumference(self):
        return 2 * self.__pi * self.__radius
circle = Circle(5)
print(circle.calculate_area())         # Output: 78.53975
print(circle.calculate_circumference())  # Output: 31.4159Exercise 8.5.3: Creating a Password Protected Account
In this exercise, you'll create an Account class that uses encapsulation to protect its data members and requires a password to access certain methods.
Instructions:
- Create a class called Account.
- Define the following private attributes: __account_number,__balance, and__password.
- Create a constructor that takes account_number,balance, andpasswordas parameters and initializes the private attributes.
- Create public methods get_account_number()andget_balance()that return the respective attributes.
- Create a method validate_password(self, password)that returnsTrueif the given password matches the account's password, andFalseotherwise.
- Create a method withdraw(self, amount, password)that checks if the password is correct usingvalidate_password(), and if so, subtracts the given amount from the balance.
Solution:
class Account:
    def __init__(self, account_number, balance, password):
        self.__account_number = account_number
        self.__balance = balance
        self.__password = password
    def get_account_number(self):
        return self.__account_number
    def get_balance(self):
        return self.__balance
    def validate_password(self, password):
        return self.__password == password
    def withdraw(self, amount, password):
        if self.validate_password(password):
            self.__balance -= amount
            return True
        return False
account = Account("123456", 1000, "secret123")
print(account.get_account_number())  # Output: 123456
print(account.get_balance())         # Output: 1000
if account.withdraw(200, "secret123"):
    print("Withdrawal successful!")
    print("New balance:", account.get_balance())  # Output: 800
else:
    print("Incorrect password or insufficient funds.")In this exercise, you've created an Account class that demonstrates the concept of encapsulation by protecting its data members and requiring a password for certain operations.
As we conclude Chapter 8, it's important to recognize the significance of Object-Oriented Programming (OOP) in Python. Through OOP, we can create more organized, maintainable, and scalable code. In this chapter, we have explored the key OOP concepts:
- Classes and Objects: The fundamentals of creating custom data types and instances in Python.
- Attributes and Methods: How to store data and define behaviors within classes.
- Inheritance: A way to create new classes from existing ones, promoting code reusability.
- Polymorphism: Leveraging the power of inheritance and method overriding to create flexible code that can handle different types of objects.
- Encapsulation: Protecting the internal state and implementation of a class, providing a well-defined interface for interacting with it.
By applying these principles in your Python programs, you can create code that is easier to understand, debug, and extend. Don't forget to practice implementing these concepts through exercises and real-world projects to gain a deeper understanding of OOP in Python.
As you move forward, remember that Python is a versatile language that supports multiple programming paradigms. Combining OOP with other approaches, such as functional programming, can help you further refine and tailor your code to suit various situations and requirements. See you in the next chapter. Happy coding!

