πŸ”„ Quick Recap (Day 13)

  • You implemented magic methods like __str__, __add__, __sub__, and __eq__.

  • You built intuitive classes that behave like built-in types.

🎯 What You’ll Learn Today

  1. What encapsulation means and how to make attributes private.

  2. How to use getter and setter methods to control access.

  3. What decorators are and how they modify functions or methods.

  4. How to write and apply a custom decorator.

πŸ“– Overview: Encapsulation

Encapsulation hides the internal state of an object and only exposes what’s necessary. In Python, you can prefix attribute names with underscores to indicate they’re private:

class Person:
    def __init__(self, name, age):
        self._name = name      # β€œprotected” convention
        self.__age = age       # β€œprivate” name mangling
  • Single underscore _name suggests β€œinternal use.”

  • Double underscore __age invokes name mangling to prevent accidental access.

Getters and Setters

Use property decorator to create controlled access:

class Person:
    def __init__(self, name, age):
        self._name = name
        self.__age = age

    @property
    def age(self):
        return self.__age

    @age.setter
    def age(self, value):
        if value < 0:
            raise ValueError("Age cannot be negative")
        self.__age = value
  • @property turns age() into a getter.

  • @age.setter defines the setter for age.

πŸ“– Overview: Decorators

A decorator is a function that takes another function and extends its behavior without modifying its code. Decorators use the @decorator_name syntax.

Example: A simple timing decorator

import time

def timer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"Executed {func.__name__} in {end-start:.4f} seconds")
        return result
    return wrapper

@timer
def compute_power(x, y):
    return x ** y

# Using the decorator
compute_power(2, 10)

This prints the execution time each time compute_power is called.

πŸ§™β€β™‚οΈ Take the Wand and Try Yourself

  1. Create a file named encap_deco.py.

  2. Define a BankAccount class:

    • Private attribute __balance initialized to 0.

    • Property balance to get the balance.

    • Setter balance to allow deposits (value > 0) and raise error otherwise.

  3. Write a decorator log_action that prints the method name and timestamp before calling any method.

  4. Apply @log_action to deposit(amount) and withdraw(amount) methods in BankAccount.

Solution Example (encap_deco.py):

# encap_deco.py
import time

# Decorator
def log_action(func):
    def wrapper(self, *args, **kwargs):
        print(f"{time.strftime('%H:%M:%S')} - Calling {func.__name__}")
        return func(self, *args, **kwargs)
    return wrapper

class BankAccount:
    def __init__(self):
        self.__balance = 0

    @property
    def balance(self):
        return self.__balance

    @balance.setter
    def balance(self, value):
        if value < 0:
            raise ValueError("Cannot set negative balance")
        self.__balance = value

    @log_action
    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("Deposit must be positive")
        self.__balance += amount

    @log_action
    def withdraw(self, amount):
        if amount <= 0 or amount > self.__balance:
            raise ValueError("Invalid withdrawal amount")
        self.__balance -= amount

# Practice
account = BankAccount()
account.deposit(100)
print("Balance:", account.balance)
account.withdraw(30)
print("Balance:", account.balance)

Expected output:

HH:MM:SS - Calling deposit
Balance: 100
HH:MM:SS - Calling withdraw
Balance: 70

Run:

python encap_deco.py

Once you see the correct logged actions and balances, you’ve mastered encapsulation and decorators!

Up next: Day 15: File I/O & Exception Handling

Keep Reading