- Understand encapsulation and data hiding
- Use private attributes and methods
- Create property getters and setters
- Understand abstraction and abstract classes
Encapsulation and Abstraction
Imagine you're driving a car. You don't need to know how the engine works internally – you just use the steering wheel, pedals, and dashboard. The complex machinery is hidden from you, and you interact with it through a simple interface.
This is encapsulation and abstraction in action. They protect data integrity and hide complexity, making your code safer and easier to use.
Encapsulation: Protecting Data
Encapsulation bundles data and methods together, and controls access to that data:
class BankAccount:
def __init__(self, owner, initial_balance):
self.owner = owner
self.__balance = initial_balance # Private attribute
self.__account_number = self.__generate_account_number()
def deposit(self, amount):
if amount > 0:
self.__balance += amount
return f"Deposited ${amount}"
return "Invalid amount"
def withdraw(self, amount):
if 0 < amount <= self.__balance:
self.__balance -= amount
return f"Withdrew ${amount}"
return "Insufficient funds or invalid amount"
def get_balance(self):
return f"Balance: ${self.__balance}"
def __generate_account_number(self):
"""Private method - internal implementation detail."""
import random
return f"ACC{random.randint(10000, 99999)}"
Accessing Private Attributes
account = BankAccount("Alice", 1000)
# Public access works
print(account.get_balance()) # "Balance: $1000"
print(account.owner) # "Alice"
# Direct access to private attributes (not recommended)
print(account._BankAccount__balance) # $1000 (name mangling)
# But this is better - use the public interface
account.deposit(500)
print(account.get_balance()) # "Balance: $1500"
Name Mangling
Python uses name mangling to make private attributes harder to access:
class Example:
def __init__(self):
self.__private = "secret"
self._protected = "semi-private"
self.public = "open"
obj = Example()
# Name mangling in action
print(obj.__dict__)
# {'_Example__private': 'secret', '_protected': 'semi-private', 'public': 'open'}
# Accessing mangled name (not recommended)
print(obj._Example__private) # "secret"
Convention:
__attribute→ Private (name mangled)_attribute→ Protected (convention only)attribute→ Public
Properties: Controlled Access
Properties allow you to control how attributes are accessed and modified:
class Temperature:
def __init__(self, celsius=0):
self._celsius = celsius
@property
def celsius(self):
"""Getter for celsius."""
return self._celsius
@celsius.setter
def celsius(self, value):
"""Setter for celsius with validation."""
if value < -273.15:
raise ValueError("Temperature cannot be below absolute zero")
self._celsius = value
@property
def fahrenheit(self):
"""Computed property - no setter needed."""
return (self._celsius * 9/5) + 32
@fahrenheit.setter
def fahrenheit(self, value):
"""Setter that converts Fahrenheit to Celsius."""
self._celsius = (value - 32) * 5/9
Using Properties
temp = Temperature(20)
print(temp.celsius) # 20 (getter)
print(temp.fahrenheit) # 68.0 (computed)
temp.celsius = 25 # setter
print(temp.fahrenheit) # 77.0
temp.fahrenheit = 86 # Fahrenheit setter
print(temp.celsius) # 30.0 (converted back)
# Validation works
# temp.celsius = -300 # ValueError.
Abstraction: Hiding Complexity
Abstraction focuses on what an object does, not how it does it:
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
"""Abstract base class for payment processing."""
@abstractmethod
def process_payment(self, amount, card_info):
"""Process a payment - must be implemented by subclasses."""
pass
@abstractmethod
def refund_payment(self, transaction_id, amount):
"""Refund a payment - must be implemented by subclasses."""
pass
def log_transaction(self, transaction_id, amount):
"""Concrete method - shared implementation."""
print(f"Transaction {transaction_id}: ${amount}")
class StripeProcessor(PaymentProcessor):
def process_payment(self, amount, card_info):
# Complex Stripe API logic here...
print(f"Processing ${amount} via Stripe")
return "stripe_txn_123"
def refund_payment(self, transaction_id, amount):
# Complex refund logic...
print(f"Refunding ${amount} for {transaction_id}")
return True
class PayPalProcessor(PaymentProcessor):
def process_payment(self, amount, card_info):
# Complex PayPal API logic...
print(f"Processing ${amount} via PayPal")
return "paypal_txn_456"
def refund_payment(self, transaction_id, amount):
# Complex refund logic...
print(f"Refunding ${amount} for {transaction_id}")
return True
Using Abstraction
# Client code doesn't need to know implementation details
def checkout(processor, amount, card_info):
transaction_id = processor.process_payment(amount, card_info)
processor.log_transaction(transaction_id, amount)
return transaction_id
# Works with any payment processor
stripe = StripeProcessor()
paypal = PayPalProcessor()
checkout(stripe, 99.99, "card_info") # Uses Stripe
checkout(paypal, 49.99, "card_info") # Uses PayPal
Email System Example
from abc import ABC, abstractmethod
class EmailService(ABC):
"""Abstract email service."""
@abstractmethod
def send_email(self, to, subject, body):
pass
def validate_email(self, email):
"""Concrete validation method."""
return "@" in email and "." in email
class SMTPEmailService(EmailService):
def __init__(self, smtp_server, username, password):
self.__smtp_server = smtp_server # Private
self.__username = username
self.__password = password
def send_email(self, to, subject, body):
if not self.validate_email(to):
raise ValueError("Invalid email address")
# SMTP logic here...
print(f"Sent email to {to} via SMTP")
class MockEmailService(EmailService):
def __init__(self):
self.__sent_emails = [] # Private storage
def send_email(self, to, subject, body):
if not self.validate_email(to):
raise ValueError("Invalid email address")
# Store for testing
self.__sent_emails.append({"to": to, "subject": subject, "body": body})
print(f"Mock email sent to {to}")
def get_sent_emails(self):
"""Getter for testing."""
return self.__sent_emails.copy()
# Usage
def send_welcome_email(service, user_email):
"""High-level function - doesn't know implementation."""
service.send_email(
user_email,
"Welcome!",
"Thanks for joining our platform."
)
# Production
smtp_service = SMTPEmailService("smtp.gmail.com", "user", "pass")
send_welcome_email(smtp_service, "user@example.com")
# Testing
mock_service = MockEmailService()
send_welcome_email(mock_service, "test@example.com")
print("Emails sent in test:", len(mock_service.get_sent_emails()))
Benefits of Encapsulation and Abstraction
1. Data Protection
class UserAccount:
def __init__(self, username, password):
self.username = username
self.__password_hash = self.__hash_password(password)
def __hash_password(self, password):
"""Private method for password hashing."""
# Complex hashing logic...
return f"hashed_{password}"
def check_password(self, password):
"""Public interface for password checking."""
return self.__password_hash == f"hashed_{password}"
user = UserAccount("alice", "secret123")
print(user.check_password("secret123")) # True
# user.__password_hash is protected from direct access
2. Interface Stability
class DatabaseConnection:
def __init__(self, connection_string):
self.__connection = self.__create_connection(connection_string)
def query(self, sql):
"""Public interface - implementation can change."""
return self.__execute_query(sql)
def __create_connection(self, conn_str):
"""Private - can change without breaking client code."""
# Implementation details...
return f"connection_to_{conn_str}"
def __execute_query(self, sql):
"""Private - can change without breaking client code."""
# Implementation details...
return f"results_for_{sql}"
# Client code only depends on the public query() method
db = DatabaseConnection("postgresql://...")
results = db.query("SELECT * FROM users")
Key Points to Remember
- Encapsulation = Bundle data and methods together, control access using private attributes (
__attribute) - Properties = Controlled attribute access using
@propertyand@attribute.setter - Abstraction = Hide complexity, show interface using abstract base classes with
@abstractmethod - Benefits = Data protection, maintainability, flexibility
- Convention:
_protected,__private,public
You've learned the core principles of OOP. Python has some special methods that make objects behave like built-in types. In the next lesson, we'll explore special methods and properties – the magic behind len(), +, and more.
