In this blog post, we're diving into the concept of Encapsulation in Python. To make things easy to understand, we'll use the example of a Vending Machine. We'll explore what encapsulation is, when to use it, and why it's essential. We'll also learn about getters and setters, as well as the @property decorator.
What is Encapsulation in Python?
Encapsulation in Python refers to the practice of hiding the internal workings of an object. This means that you can bundle instance variables (often called attributes) and methods into a single entity called a Class.
Encapsulation allows us to control the access to these attributes and methods, making some public and some private. This way, we hide the inner workings of an object, exposing only what's necessary. Think of it like a vending machine, you don't need to know how it works on the inside to use it.
The Vending Machine Example
In this code snippet, we have a simple VendingMachine class that demonstrates how a vending machine operates. We've included methods for showing inventory, inserting money, making a purchase, and getting change. Please remember, as a user, we don't care about how the vending machine works. We just look at the items, insert the money, get our item, and receive any remaining balance.
class VendingMachine:
def __init__(self):
self.inventory = {'coke': 5, 'crisps': 3, 'chocolate': 10}
self.item_price = 1
self.balance = 0
def show_inventory(self):
print("Available items")
print("---------------")
for item, count in self.inventory.items():
print(f"{item}: {count}")
def insert_money(self, amount):
self.balance += amount
print(f"Inserted - £{amount}")
def purchase(self, item_name):
if item_name in self.inventory and self.inventory[item_name] > 0 and self.balance >= self.item_price:
self.balance -= self.item_price
self.inventory[item_name] -= 1
print(f"Purchased '{item_name}' | Remaining balance: £{self.balance}")
elif item_name in self.inventory and self.inventory[item_name] <= 0:
print("Item not available")
elif item_name not in self.inventory:
print("Invalid item selected")
elif self.balance < self.item_price:
print("Not enough balance")
def get_change(self):
if self.balance != 0:
print(f"Returning £{self.balance} change")
self.balance = 0.0
else:
print(f"No Change")
# Example usage
vm = VendingMachine()
vm.show_inventory()
vm.insert_money(5)
vm.purchase('coke')
vm.get_change()
#output
Available items
---------------
coke: 5
crisps: 3
chocolate: 10
Inserted - £5
Purchased 'coke' | Remaining balance: £4
Returning £4 change
In our example VendingMachine class, we start by setting up the inventory with some items like 'coke', 'crisps', and 'chocolate', each with its quantity. We also set a fixed item price and initialize a balance.
Then we have four methods. The show_inventory
method prints the available items. The insert_money
method lets us add money to the balance. The purchase
method allows us to buy an item if it's in stock and we have enough balance. Finally, the get_change
method returns any remaining balance. With these methods, the class mimics the basic operations you would expect from a real-life vending machine.
The Problem with this Approach
While our VendingMachine class works well for basic operations, there's a glaring issue. Anyone using the class can directly change the instance variables like balance
or item_price
.
For example, you could easily set the balance to £50 just by using vm.balance = 50
, without actually inserting any money. This violates the basic principle that internal details like balance should be controlled and managed within the class itself, to prevent unintended changes or misuse.
vm = VendingMachine()
vm.balance = 50
print(vm.balance)
#output
50
Sometimes the shop worker also might need to manually adjust the machine's settings. Imagine a customer's coin getting stuck. Right now, the only way to amend the balance is by accessing the instance variable directly, like vm.balance = 5
. But according to strict encapsulation practices, you shouldn't really be accessing these variables directly. It's like opening up the vending machine with a crowbar instead of using a key—you can do it, but it's not the best approach.
Why You Should Avoid Direct Access?
In Python, encapsulation is often viewed as a form of "information hiding" where the inner workings of a class stay hidden from the outside world. This approach allows you to control how the data within the class can be accessed or modified. While it's tempting to access a class's internal variables or methods directly, doing so can lead to unpredictable behaviour and errors.
Direct access could accidentally modify variables in a way that makes the entire system unstable or insecure. Even though Python gives you the flexibility to access a class's attributes and methods directly, it's often best to use encapsulation to create some boundaries for safer and more predictable coding.
Getters and Setters
Getters and Setters are special methods that help us safely access and modify instance variables. A Getter allows you to 'get' or retrieve the value of a variable. A Setter, on the other hand, lets you 'set' or update the value. This way, instead of accessing or changing variables directly, you use these methods. It's like having a controlled mechanism for our vending machine's balance or inventory. With Getters and Setters, we can also add checks or additional logic. Here is the updated code.
class VendingMachine:
def __init__(self):
self._inventory = {'coke': 5, 'crisps': 3, 'chocolate': 10}
self._item_price = 1
self._balance = 0
def show_inventory(self):
print("Available items")
print("---------------")
for item, count in self._inventory.items():
print(f"{item}: {count}")
# Getter for balance
def get_balance(self):
return self._balance
# Setter for balance
def set_balance(self, amount):
if amount >= 0:
self._balance = amount
else:
print("Invalid amount")
def insert_money(self, amount):
self._balance += amount
print(f"Inserted - £{amount} | Total Balance - £{self._balance}")
def purchase(self, item_name):
if item_name in self._inventory and self._inventory[item_name] > 0 and self._balance >= self._item_price:
self._balance -= self._item_price
self._inventory[item_name] -= 1
print(f"Purchased '{item_name}' | Remaining balance: £{self._balance}")
elif item_name in self._inventory and self._inventory[item_name] <= 0:
print("Item not available")
elif item_name not in self._inventory:
print("Invalid item selected")
elif self._balance < self._item_price:
print("Not enough balance")
def get_change(self):
if self.get_balance() > 0:
print(f"Returning £{self._balance} change")
self._balance = 0
else:
print("No change")
# Example usage
vm = VendingMachine()
vm.set_balance(5)
print(vm.get_balance())
#output
5
In the updated version of our Vending Machine code, we've introduced getters and setters for managing the balance. Specifically, we added a get_balance
method that lets us see the current balance and a set_balance
method to update it. By using these methods, we add an extra layer of control. For example, in the set_balance
method, we check if the amount is greater than or equal to zero before updating it.
Private (_ and __) Variables
If you try to directly access and modify the instance variable using vm.balance = 10
, you'll actually end up creating a new instance variable named balance
for that specific object, separate from the original _balance
variable we intended to use. When you later call vm.get_balance()
, it will still return the value of _balance
, not the new balance you've just set.
vm = VendingMachine()
vm.balance = 10
print(vm.get_balance())
#output
0
In Python, prepending a single underscore _
to a variable name is more of a convention than a strict rule for making a variable private. This means that the variable can still be accessed and modified directly from outside the class, but the underscore indicates to the developer that it's meant for internal use within the class. As you can see below, I can still modify the instance variable by calling the _balance
instance variable.
vm = VendingMachine()
vm._balance = 10
print(vm.get_balance())
#output
10
This is different from some other languages that have strict private and public designations. In Python, the single underscore is more like a "gentleman's agreement" that the variable should not be accessed directly, rather than a strict prohibition.
On the other hand, using double underscores __
before a variable name will name-mangle the attribute name. This is closer to what is considered "private" in many other programming languages. The interpreter changes the name of the variable in a way that makes it harder to create subclasses that accidentally override the private attributes and methods. For example, if you have __balance
, it might become _VendingMachine__balance
internally. However, it's worth noting that this doesn't make it completely private; it just makes it harder to access unintentionally.
Even with name-mangling, you can still access and modify the variable if you know the name the interpreter changes it to. For example, if you've used __balance
in a class named VendingMachine
, you can still access it from outside the class with vm._VendingMachine__balance
. It's a way to help prevent accidental access but not a way to strictly enforce privacy.
@property Decorator
The @property
decorator in Python allows you to manage instance variables in an object-oriented way. When you use @property
, you can access a method as if it's a simple attribute, allowing you to replace what could have been direct attribute access with method-based access.
You can also define a corresponding setter method for it using @<method_name>.setter
. This lets you run checks or operations before actually setting the attribute. So, with @property
and its setter, you can both retrieve and modify an attribute's value while incorporating any needed logic or checks, all wrapped up in a clean and Pythonic way.
class VendingMachine:
def __init__(self):
self._inventory = {'coke': 5, 'crisps': 3, 'chocolate': 10}
self._item_price = 1
self._balance = 0
def show_inventory(self):
print("Available items")
print("---------------")
for item, count in self._inventory.items():
print(f"{item}: {count}")
@property
def balance(self):
return self._balance
@balance.setter
def balance(self, amount):
if amount >= 0:
self._balance = amount
else:
print("Invalid amount")
def insert_money(self, amount):
self.balance += amount
print(f"Inserted - £{amount} | Total Balance - £{self.balance}")
def purchase(self, item_name):
if item_name in self._inventory and self._inventory[item_name] > 0 and self.balance >= self._item_price:
self.balance -= self._item_price
self._inventory[item_name] -= 1
print(f"Purchased '{item_name}' | Remaining balance: £{self.balance}")
else:
print("Transaction failed for some reason")
def get_change(self):
if self.balance > 0:
print(f"Returning £{self.balance} change")
self.balance = 0
else:
print("No change")
vm = VendingMachine()
vm.balance = 10
print(vm.balance)
#output
10
As you can see above, we could replace the get_balance
and set_balance
methods with the @property
decorator. This would allow us to access the _balance
variable as if it were a public attribute, while still giving us control over its value. So when you do vm.balance
, it would internally call the method decorated with @property
, effectively serving as a "getter".
Similarly, when you set a value like vm.balance = 10
, it would call the method decorated with @balance.setter
, acting as a "setter". This way, you get a neat and clean approach to controlling access to the balance, without the need to explicitly call get_balance
or set_balance
. It's a cleaner and more Pythonic way to handle encapsulation.
Closing Up
Ah, that's a lot to cover, isn't it? You might be wondering which method is best for your project, using _
or __
for variable names, or maybe going for the @property
and @<method_name>.setter
decorators? Well, it really depends on what you need.
If you want a simple way to indicate that a variable shouldn't be accessed directly, then using a single underscore is quick and straightforward. But if you're looking for a way to control variable access in a more polished manner, @property
is the way to go. It's a bit more work upfront, but it makes your code easier to read and maintain in the long run. So, choose the approach that fits your project's needs the best!