Menu iconMenu iconPython Programming Unlocked for Beginners
Python Programming Unlocked for Beginners

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: 1200

In 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:

  1. Create a class called Employee.
  2. Define the following private attributes: __first_name__last_name, and __salary.
  3. Create a constructor that takes first_namelast_name, and salary as parameters and initializes the private attributes.
  4. Create public methods get_first_name()get_last_name(), and get_salary() that return the respective attributes.
  5. 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 Doe

Exercise 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:

  1. Create a class called Circle.
  2. Define the following private attributes: __radius and __pi (use the value 3.14159 for pi).
  3. Create a constructor that takes radius as a parameter and initializes the private attribute __radius.
  4. Create public methods get_radius() and get_pi() that return the respective attributes.
  5. Create methods calculate_area() and calculate_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.4159

Exercise 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:

  1. Create a class called Account.
  2. Define the following private attributes: __account_number__balance, and __password.
  3. Create a constructor that takes account_numberbalance, and password as parameters and initializes the private attributes.
  4. Create public methods get_account_number() and get_balance() that return the respective attributes.
  5. Create a method validate_password(self, password) that returns True if the given password matches the account's password, and False otherwise.
  6. Create a method withdraw(self, amount, password) that checks if the password is correct using validate_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:

  1. Classes and Objects: The fundamentals of creating custom data types and instances in Python.
  2. Attributes and Methods: How to store data and define behaviors within classes.
  3. Inheritance: A way to create new classes from existing ones, promoting code reusability.
  4. Polymorphism: Leveraging the power of inheritance and method overriding to create flexible code that can handle different types of objects.
  5. 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: 1200

In 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:

  1. Create a class called Employee.
  2. Define the following private attributes: __first_name__last_name, and __salary.
  3. Create a constructor that takes first_namelast_name, and salary as parameters and initializes the private attributes.
  4. Create public methods get_first_name()get_last_name(), and get_salary() that return the respective attributes.
  5. 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 Doe

Exercise 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:

  1. Create a class called Circle.
  2. Define the following private attributes: __radius and __pi (use the value 3.14159 for pi).
  3. Create a constructor that takes radius as a parameter and initializes the private attribute __radius.
  4. Create public methods get_radius() and get_pi() that return the respective attributes.
  5. Create methods calculate_area() and calculate_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.4159

Exercise 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:

  1. Create a class called Account.
  2. Define the following private attributes: __account_number__balance, and __password.
  3. Create a constructor that takes account_numberbalance, and password as parameters and initializes the private attributes.
  4. Create public methods get_account_number() and get_balance() that return the respective attributes.
  5. Create a method validate_password(self, password) that returns True if the given password matches the account's password, and False otherwise.
  6. Create a method withdraw(self, amount, password) that checks if the password is correct using validate_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:

  1. Classes and Objects: The fundamentals of creating custom data types and instances in Python.
  2. Attributes and Methods: How to store data and define behaviors within classes.
  3. Inheritance: A way to create new classes from existing ones, promoting code reusability.
  4. Polymorphism: Leveraging the power of inheritance and method overriding to create flexible code that can handle different types of objects.
  5. 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: 1200

In 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:

  1. Create a class called Employee.
  2. Define the following private attributes: __first_name__last_name, and __salary.
  3. Create a constructor that takes first_namelast_name, and salary as parameters and initializes the private attributes.
  4. Create public methods get_first_name()get_last_name(), and get_salary() that return the respective attributes.
  5. 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 Doe

Exercise 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:

  1. Create a class called Circle.
  2. Define the following private attributes: __radius and __pi (use the value 3.14159 for pi).
  3. Create a constructor that takes radius as a parameter and initializes the private attribute __radius.
  4. Create public methods get_radius() and get_pi() that return the respective attributes.
  5. Create methods calculate_area() and calculate_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.4159

Exercise 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:

  1. Create a class called Account.
  2. Define the following private attributes: __account_number__balance, and __password.
  3. Create a constructor that takes account_numberbalance, and password as parameters and initializes the private attributes.
  4. Create public methods get_account_number() and get_balance() that return the respective attributes.
  5. Create a method validate_password(self, password) that returns True if the given password matches the account's password, and False otherwise.
  6. Create a method withdraw(self, amount, password) that checks if the password is correct using validate_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:

  1. Classes and Objects: The fundamentals of creating custom data types and instances in Python.
  2. Attributes and Methods: How to store data and define behaviors within classes.
  3. Inheritance: A way to create new classes from existing ones, promoting code reusability.
  4. Polymorphism: Leveraging the power of inheritance and method overriding to create flexible code that can handle different types of objects.
  5. 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: 1200

In 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:

  1. Create a class called Employee.
  2. Define the following private attributes: __first_name__last_name, and __salary.
  3. Create a constructor that takes first_namelast_name, and salary as parameters and initializes the private attributes.
  4. Create public methods get_first_name()get_last_name(), and get_salary() that return the respective attributes.
  5. 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 Doe

Exercise 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:

  1. Create a class called Circle.
  2. Define the following private attributes: __radius and __pi (use the value 3.14159 for pi).
  3. Create a constructor that takes radius as a parameter and initializes the private attribute __radius.
  4. Create public methods get_radius() and get_pi() that return the respective attributes.
  5. Create methods calculate_area() and calculate_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.4159

Exercise 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:

  1. Create a class called Account.
  2. Define the following private attributes: __account_number__balance, and __password.
  3. Create a constructor that takes account_numberbalance, and password as parameters and initializes the private attributes.
  4. Create public methods get_account_number() and get_balance() that return the respective attributes.
  5. Create a method validate_password(self, password) that returns True if the given password matches the account's password, and False otherwise.
  6. Create a method withdraw(self, amount, password) that checks if the password is correct using validate_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:

  1. Classes and Objects: The fundamentals of creating custom data types and instances in Python.
  2. Attributes and Methods: How to store data and define behaviors within classes.
  3. Inheritance: A way to create new classes from existing ones, promoting code reusability.
  4. Polymorphism: Leveraging the power of inheritance and method overriding to create flexible code that can handle different types of objects.
  5. 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!