Python - Access Modifiers
Hello there, aspiring Python programmers! Today, we're going to embark on an exciting journey into the world of Access Modifiers in Python. Don't worry if you're new to programming; I'll guide you through this concept step by step, with plenty of examples and explanations along the way. So, let's dive in!

Access Modifiers in Python
In object-oriented programming, access modifiers are used to control the visibility and accessibility of class members (attributes and methods). While many programming languages have strict access modifiers like public, private, and protected, Python takes a more relaxed approach. It follows a philosophy often referred to as "We're all consenting adults here."
In Python, we have three types of access modifiers:
- Public
 - Protected
 - Private
 
Let's explore each of these with examples.
Public Members
In Python, all members are public by default. This means they can be accessed from outside the class. Here's an example:
class Student:
    def __init__(self, name, age):
        self.name = name  # Public attribute
        self.age = age    # Public attribute
    def display_info(self):  # Public method
        print(f"Name: {self.name}, Age: {self.age}")
# Creating an instance of Student
student1 = Student("Alice", 20)
# Accessing public members
print(student1.name)  # Output: Alice
student1.display_info()  # Output: Name: Alice, Age: 20
In this example, name, age, and display_info() are all public members. We can access them directly from outside the class.
Protected Members
Protected members are denoted by prefixing the member name with a single underscore (_). They are not truly private and can still be accessed from outside the class, but it's a convention to treat them as internal use only.
class Employee:
    def __init__(self, name, salary):
        self._name = name      # Protected attribute
        self._salary = salary  # Protected attribute
    def _display_salary(self):  # Protected method
        print(f"{self._name}'s salary is ${self._salary}")
# Creating an instance of Employee
emp1 = Employee("Bob", 50000)
# Accessing protected members (Note: This is possible but not recommended)
print(emp1._name)  # Output: Bob
emp1._display_salary()  # Output: Bob's salary is $50000
While we can access _name, _salary, and _display_salary(), it's generally not recommended to do so outside the class or its subclasses.
Private Members
Private members are denoted by prefixing the name with double underscores (__). Python performs name mangling for these members, making them harder (but not impossible) to access from outside the class.
class BankAccount:
    def __init__(self, account_number, balance):
        self.__account_number = account_number  # Private attribute
        self.__balance = balance                # Private attribute
    def __display_balance(self):  # Private method
        print(f"Balance: ${self.__balance}")
    def public_display(self):
        self.__display_balance()
# Creating an instance of BankAccount
account1 = BankAccount("123456", 1000)
# Trying to access private members
# print(account1.__account_number)  # This will raise an AttributeError
# account1.__display_balance()      # This will also raise an AttributeError
# Accessing private method through a public method
account1.public_display()  # Output: Balance: $1000
In this example, __account_number, __balance, and __display_balance() are private members. Attempting to access them directly from outside the class will raise an AttributeError.
Name Mangling
Remember when I mentioned that private members in Python are not truly private? This is because of a mechanism called name mangling. When you create a private member using double underscores, Python changes its name internally to make it harder to access accidentally.
Here's how it works:
class NameManglingDemo:
    def __init__(self):
        self.__private_var = "I'm private!"
demo = NameManglingDemo()
print(dir(demo))
# Output: [..., '_NameManglingDemo__private_var', ...]
# Accessing the private variable using the mangled name
print(demo._NameManglingDemo__private_var)  # Output: I'm private!
As you can see, Python renames __private_var to _NameManglingDemo__private_var. This is name mangling in action!
Python Property Object
The property() function in Python is a built-in function that creates and returns a property object. It's a way to add getter, setter, and deleter methods to class attributes.
Here's an example:
class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius
    def get_fahrenheit(self):
        return (self._celsius * 9/5) + 32
    def set_fahrenheit(self, fahrenheit):
        self._celsius = (fahrenheit - 32) * 5/9
    fahrenheit = property(get_fahrenheit, set_fahrenheit)
# Using the property
temp = Temperature(25)
print(temp.fahrenheit)  # Output: 77.0
temp.fahrenheit = 86
print(temp._celsius)  # Output: 30.0
In this example, fahrenheit is a property that allows us to get and set the temperature in Fahrenheit while internally storing it in Celsius.
Getters and Setter Methods
Getters and setters are methods used to get and set the values of class attributes. They provide a way to access and modify private attributes while maintaining encapsulation.
Here's an example using the @property decorator, which is a more Pythonic way to implement getters and setters:
class Person:
    def __init__(self, name, age):
        self._name = name
        self._age = age
    @property
    def name(self):
        return self._name
    @name.setter
    def name(self, value):
        if not isinstance(value, str):
            raise ValueError("Name must be a string")
        self._name = value
    @property
    def age(self):
        return self._age
    @age.setter
    def age(self, value):
        if not isinstance(value, int) or value < 0:
            raise ValueError("Age must be a positive integer")
        self._age = value
# Using getters and setters
person = Person("Charlie", 30)
print(person.name)  # Output: Charlie
person.name = "David"
print(person.name)  # Output: David
try:
    person.age = -5
except ValueError as e:
    print(e)  # Output: Age must be a positive integer
In this example, we've created getter and setter methods for name and age. The setter methods include validation to ensure that the values being set meet certain criteria.
To summarize the methods we've discussed, here's a table in Markdown format:
| Method | Description | Example | 
|---|---|---|
| Public | Accessible from anywhere | self.name | 
| Protected | Accessible within class and subclasses (by convention) | self._name | 
| Private | Name mangled to restrict access | self.__name | 
| Property | Creates a property object | property(get_method, set_method) | 
| Getter | Method to get the value of an attribute | @property | 
| Setter | Method to set the value of an attribute | @attribute.setter | 
And there you have it! We've covered access modifiers in Python, name mangling, property objects, and getter and setter methods. Remember, Python's approach to access control is more about convention and trust than strict rules. As you continue your Python journey, you'll find that this flexibility allows for clean and readable code while still providing ways to implement encapsulation when needed.
Keep practicing, stay curious, and happy coding!
Credits: Image by storyset
