Skip to content

Latest commit

 

History

History
741 lines (578 loc) · 24.2 KB

File metadata and controls

741 lines (578 loc) · 24.2 KB

Python Instance Methods: Complete Guide

Instance methods are functions that belong to objects and operate on their individual data through the special self parameter. They are the primary way objects exhibit behavior and interact with their own attributes and other objects. Understanding instance methods is crucial for effective object-oriented programming in Python.

Understanding Instance Methods

An instance method is a function defined inside a class that operates on individual objects (instances) of that class. Every instance method must have self as its first parameter, which Python automatically passes when you call the method.

class Counter:
    def __init__(self, start=0):
        self.value = start  # Instance attribute
    
    def increment(self):    # Instance method
        """Increase counter by 1."""
        self.value += 1     # Access instance attribute through self
        return self.value
    
    def get_value(self):    # Instance method
        """Return current counter value."""
        return self.value   # Access instance attribute through self

# Create instances and call methods
counter1 = Counter(10)
counter2 = Counter(0)

print(counter1.increment())  # 11 - Python automatically passes counter1 as self
print(counter2.increment())  # 1  - Python automatically passes counter2 as self
print(counter1.get_value())  # 11 - Each object has its own state
print(counter2.get_value())  # 1

The self Parameter

The self parameter is Python's way of referring to the specific object instance that the method is called on:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    def introduce(self):
        """Instance method using self to access attributes."""
        return f"Hi, I'm {self.name} and I'm {self.age} years old."
    
    def have_birthday(self):
        """Instance method that modifies state."""
        self.age += 1
        return f"{self.name} is now {self.age} years old!"
    
    def greet(self, other_person):
        """Instance method that takes additional parameters."""
        return f"{self.name} says hello to {other_person.name}!"

alice = Person("Alice", 25)
bob = Person("Bob", 30)

# Python automatically passes the object as self
print(alice.introduce())        # Hi, I'm Alice and I'm 25 years old.
print(bob.introduce())          # Hi, I'm Bob and I'm 30 years old.

alice.have_birthday()
print(alice.introduce())        # Hi, I'm Alice and I'm 26 years old.

print(alice.greet(bob))         # Alice says hello to Bob!

# This is what Python does behind the scenes:
# alice.introduce() is equivalent to Person.introduce(alice)

How self Works

class Example:
    def __init__(self, value):
        self.value = value
    
    def show_self(self):
        print(f"self is: {self}")
        print(f"self.value is: {self.value}")
        print(f"id(self) is: {id(self)}")

obj1 = Example("first")
obj2 = Example("second")

print(f"obj1 is: {obj1}")
print(f"id(obj1) is: {id(obj1)}")
obj1.show_self()
print()

print(f"obj2 is: {obj2}")  
print(f"id(obj2) is: {id(obj2)}")
obj2.show_self()

# Output shows that self refers to the specific object instance

Types of Instance Methods

Getter Methods

Methods that return information about the object's state:

class BankAccount:
    def __init__(self, account_number, balance=0):
        self.account_number = account_number
        self._balance = balance  # Protected attribute
        self.transaction_history = []
    
    def get_balance(self):
        """Return current account balance."""
        return self._balance
    
    def get_account_number(self):
        """Return account number."""
        return self.account_number
    
    def get_transaction_count(self):
        """Return number of transactions."""
        return len(self.transaction_history)
    
    def get_last_transaction(self):
        """Return most recent transaction, or None if no transactions."""
        if self.transaction_history:
            return self.transaction_history[-1]
        return None

account = BankAccount("123456789", 1000)
print(account.get_balance())         # 1000
print(account.get_transaction_count())  # 0

Setter/Modifier Methods

Methods that change the object's state:

class Student:
    def __init__(self, name, student_id):
        self.name = name
        self.student_id = student_id
        self.grades = {}
        self.enrollment_status = "active"
    
    def set_name(self, new_name):
        """Update student's name."""
        if not new_name or not isinstance(new_name, str):
            raise ValueError("Name must be a non-empty string")
        old_name = self.name
        self.name = new_name
        return f"Name changed from {old_name} to {new_name}"
    
    def add_grade(self, subject, grade):
        """Add a grade for a subject."""
        if not 0 <= grade <= 100:
            raise ValueError("Grade must be between 0 and 100")
        self.grades[subject] = grade
        return f"Added {subject}: {grade}"
    
    def update_grade(self, subject, new_grade):
        """Update existing grade for a subject."""
        if subject not in self.grades:
            raise KeyError(f"No existing grade for {subject}")
        if not 0 <= new_grade <= 100:
            raise ValueError("Grade must be between 0 and 100")
        old_grade = self.grades[subject]
        self.grades[subject] = new_grade
        return f"Updated {subject} from {old_grade} to {new_grade}"
    
    def withdraw(self):
        """Change enrollment status to withdrawn."""
        if self.enrollment_status == "withdrawn":
            return "Student is already withdrawn"
        self.enrollment_status = "withdrawn"
        return f"{self.name} has been withdrawn"

student = Student("Alice", "12345")
print(student.add_grade("Math", 95))        # Added Math: 95
print(student.update_grade("Math", 98))     # Updated Math from 95 to 98

Action Methods

Methods that perform operations or behaviors:

class ShoppingCart:
    def __init__(self):
        self.items = {}  # {item_name: {'price': price, 'quantity': quantity}}
        self.discount_rate = 0.0
    
    def add_item(self, item_name, price, quantity=1):
        """Add items to the cart."""
        if item_name in self.items:
            self.items[item_name]['quantity'] += quantity
        else:
            self.items[item_name] = {'price': price, 'quantity': quantity}
        return f"Added {quantity} {item_name}(s) to cart"
    
    def remove_item(self, item_name, quantity=None):
        """Remove items from the cart."""
        if item_name not in self.items:
            raise KeyError(f"{item_name} not in cart")
        
        if quantity is None or quantity >= self.items[item_name]['quantity']:
            # Remove all of this item
            removed_quantity = self.items[item_name]['quantity']
            del self.items[item_name]
            return f"Removed all {removed_quantity} {item_name}(s) from cart"
        else:
            # Remove partial quantity
            self.items[item_name]['quantity'] -= quantity
            return f"Removed {quantity} {item_name}(s) from cart"
    
    def apply_discount(self, discount_rate):
        """Apply a discount to the cart."""
        if not 0 <= discount_rate <= 1:
            raise ValueError("Discount rate must be between 0 and 1")
        self.discount_rate = discount_rate
        return f"Applied {discount_rate * 100}% discount"
    
    def checkout(self):
        """Process the cart and clear it."""
        if not self.items:
            raise ValueError("Cannot checkout empty cart")
        
        subtotal = sum(item['price'] * item['quantity'] for item in self.items.values())
        discount_amount = subtotal * self.discount_rate
        total = subtotal - discount_amount
        
        # Clear the cart
        items_purchased = len(self.items)
        self.items.clear()
        self.discount_rate = 0.0
        
        return {
            'items_purchased': items_purchased,
            'subtotal': subtotal,
            'discount': discount_amount,
            'total': total
        }

cart = ShoppingCart()
cart.add_item("Laptop", 999.99)
cart.add_item("Mouse", 29.99, 2)
cart.apply_discount(0.1)
receipt = cart.checkout()
print(f"Total: ${receipt['total']:.2f}")  # Total with 10% discount

Query Methods

Methods that return boolean values or answer questions about the object:

class Rectangle:
    def __init__(self, width, height):
        if width <= 0 or height <= 0:
            raise ValueError("Width and height must be positive")
        self.width = width
        self.height = height
    
    def is_square(self):
        """Check if this rectangle is a square."""
        return self.width == self.height
    
    def is_larger_than(self, other_rectangle):
        """Check if this rectangle has a larger area than another."""
        return self.get_area() > other_rectangle.get_area()
    
    def can_contain(self, other_rectangle):
        """Check if this rectangle can contain another rectangle."""
        return (self.width >= other_rectangle.width and 
                self.height >= other_rectangle.height)
    
    def is_similar_to(self, other_rectangle, tolerance=0.1):
        """Check if this rectangle has similar proportions to another."""
        ratio1 = self.width / self.height
        ratio2 = other_rectangle.width / other_rectangle.height
        return abs(ratio1 - ratio2) <= tolerance
    
    def get_area(self):
        """Calculate area (helper method for comparisons)."""
        return self.width * self.height

rect1 = Rectangle(4, 4)
rect2 = Rectangle(3, 6)
rect3 = Rectangle(2, 2)

print(rect1.is_square())                    # True
print(rect2.is_square())                    # False
print(rect2.is_larger_than(rect1))          # True (18 > 16)
print(rect1.can_contain(rect3))             # True
print(rect1.is_similar_to(rect3))           # True (both are squares)

Method Chaining

Methods can return self to enable fluent interfaces and method chaining:

class TextProcessor:
    def __init__(self, text=""):
        self.text = text
    
    def add_text(self, new_text):
        """Add text and return self for chaining."""
        self.text += new_text
        return self
    
    def uppercase(self):
        """Convert to uppercase and return self for chaining."""
        self.text = self.text.upper()
        return self
    
    def replace(self, old, new):
        """Replace text and return self for chaining."""
        self.text = self.text.replace(old, new)
        return self
    
    def trim(self):
        """Remove whitespace and return self for chaining."""
        self.text = self.text.strip()
        return self
    
    def get_result(self):
        """Return the final result."""
        return self.text
    
    def __str__(self):
        """String representation."""
        return self.text

# Method chaining in action
processor = TextProcessor("  hello world  ")
result = (processor
          .trim()
          .uppercase()
          .replace("WORLD", "PYTHON")
          .add_text("!")
          .get_result())

print(result)  # HELLO PYTHON!

# Each method call returns the object, allowing chaining

Methods Calling Other Methods

Instance methods can call other methods on the same object:

class Calculator:
    def __init__(self):
        self.value = 0
        self.history = []
    
    def _record_operation(self, operation, operand, result):
        """Private helper method to record operations."""
        self.history.append(f"{operation} {operand} = {result}")
    
    def add(self, number):
        """Add a number to the current value."""
        old_value = self.value
        self.value += number
        self._record_operation(f"{old_value} +", number, self.value)
        return self
    
    def multiply(self, number):
        """Multiply current value by a number."""
        old_value = self.value
        self.value *= number
        self._record_operation(f"{old_value} *", number, self.value)
        return self
    
    def power(self, exponent):
        """Raise current value to a power."""
        old_value = self.value
        self.value = self.value ** exponent
        self._record_operation(f"{old_value} ^", exponent, self.value)
        return self
    
    def reset(self):
        """Reset calculator and clear history."""
        self.value = 0
        self.history.clear()
        return self
    
    def get_value(self):
        """Get current value."""
        return self.value
    
    def get_history(self):
        """Get operation history."""
        return self.history.copy()
    
    def undo_last(self):
        """Undo the last operation (simplified version)."""
        if not self.history:
            return self
        
        # This is a simplified undo - in real implementation, 
        # you'd store previous values
        last_op = self.history.pop()
        print(f"Undoing: {last_op}")
        return self

calc = Calculator()
calc.add(10).multiply(3).power(2)
print(f"Result: {calc.get_value()}")    # 900
print("History:")
for operation in calc.get_history():
    print(f"  {operation}")

Method Parameters and Arguments

Instance methods can accept parameters just like regular functions:

class DataProcessor:
    def __init__(self):
        self.data = []
    
    def add_data(self, *values):
        """Add multiple values using *args."""
        self.data.extend(values)
        return f"Added {len(values)} values"
    
    def filter_data(self, condition_func, keep_original=False):
        """Filter data using a function parameter."""
        if keep_original:
            return [x for x in self.data if condition_func(x)]
        else:
            self.data = [x for x in self.data if condition_func(x)]
            return f"Filtered to {len(self.data)} values"
    
    def transform_data(self, transform_func, **kwargs):
        """Transform data using function and keyword arguments."""
        self.data = [transform_func(x, **kwargs) for x in self.data]
        return f"Transformed {len(self.data)} values"
    
    def get_stats(self, include_details=True):
        """Get statistics with optional details."""
        if not self.data:
            return "No data available"
        
        stats = {
            'count': len(self.data),
            'sum': sum(self.data),
            'average': sum(self.data) / len(self.data)
        }
        
        if include_details:
            stats.update({
                'min': min(self.data),
                'max': max(self.data),
                'range': max(self.data) - min(self.data)
            })
        
        return stats

def is_positive(x):
    return x > 0

def multiply_by(x, factor=2):
    return x * factor

processor = DataProcessor()
processor.add_data(1, -2, 3, -4, 5)
processor.filter_data(is_positive)  # Keep only positive numbers
processor.transform_data(multiply_by, factor=3)  # Multiply by 3
print(processor.get_stats())  # Statistics of [3, 9, 15]

Error Handling in Methods

Methods should validate inputs and handle error conditions appropriately:

class BankAccount:
    def __init__(self, account_number, initial_balance=0):
        if not isinstance(account_number, str) or not account_number:
            raise ValueError("Account number must be a non-empty string")
        if not isinstance(initial_balance, (int, float)) or initial_balance < 0:
            raise ValueError("Initial balance must be a non-negative number")
        
        self.account_number = account_number
        self._balance = float(initial_balance)
        self._is_frozen = False
    
    def deposit(self, amount):
        """Deposit money with validation."""
        if self._is_frozen:
            raise RuntimeError("Account is frozen")
        
        if not isinstance(amount, (int, float)):
            raise TypeError("Amount must be a number")
        
        if amount <= 0:
            raise ValueError("Deposit amount must be positive")
        
        self._balance += amount
        return f"Deposited ${amount:.2f}. New balance: ${self._balance:.2f}"
    
    def withdraw(self, amount):
        """Withdraw money with validation."""
        if self._is_frozen:
            raise RuntimeError("Account is frozen")
        
        if not isinstance(amount, (int, float)):
            raise TypeError("Amount must be a number")
        
        if amount <= 0:
            raise ValueError("Withdrawal amount must be positive")
        
        if amount > self._balance:
            raise ValueError(f"Insufficient funds. Balance: ${self._balance:.2f}")
        
        self._balance -= amount
        return f"Withdrew ${amount:.2f}. New balance: ${self._balance:.2f}"
    
    def freeze_account(self):
        """Freeze the account to prevent transactions."""
        if self._is_frozen:
            return "Account is already frozen"
        self._is_frozen = True
        return "Account has been frozen"
    
    def unfreeze_account(self):
        """Unfreeze the account to allow transactions."""
        if not self._is_frozen:
            return "Account is not frozen"
        self._is_frozen = False
        return "Account has been unfrozen"
    
    def get_balance(self):
        """Get current balance."""
        return self._balance
    
    def is_frozen(self):
        """Check if account is frozen."""
        return self._is_frozen

# Using the bank account with error handling
try:
    account = BankAccount("12345", 1000)
    print(account.deposit(500))      # Success
    print(account.withdraw(2000))    # Raises ValueError: insufficient funds
except ValueError as e:
    print(f"Error: {e}")

try:
    account.freeze_account()
    account.deposit(100)             # Raises RuntimeError: account frozen
except RuntimeError as e:
    print(f"Error: {e}")

Method Documentation

Instance methods should have clear docstrings explaining their purpose, parameters, and return values:

class Temperature:
    def __init__(self, celsius=0):
        """Initialize temperature in Celsius.
        
        Args:
            celsius (float): Temperature in Celsius. Defaults to 0.
            
        Raises:
            ValueError: If temperature is below absolute zero.
        """
        if celsius < -273.15:
            raise ValueError("Temperature cannot be below absolute zero (-273.15°C)")
        self._celsius = celsius
    
    def convert_to_fahrenheit(self):
        """Convert temperature to Fahrenheit.
        
        Returns:
            float: Temperature in Fahrenheit.
            
        Example:
            >>> temp = Temperature(25)
            >>> temp.convert_to_fahrenheit()
            77.0
        """
        return (self._celsius * 9/5) + 32
    
    def convert_to_kelvin(self):
        """Convert temperature to Kelvin.
        
        Returns:
            float: Temperature in Kelvin.
            
        Example:
            >>> temp = Temperature(25)
            >>> temp.convert_to_kelvin()
            298.15
        """
        return self._celsius + 273.15
    
    def is_freezing(self, substance="water"):
        """Check if substance would freeze at this temperature.
        
        Args:
            substance (str): The substance to check. Currently only supports "water".
                           Defaults to "water".
        
        Returns:
            bool: True if the substance would freeze, False otherwise.
            
        Raises:
            ValueError: If substance is not supported.
            
        Example:
            >>> temp = Temperature(-5)
            >>> temp.is_freezing()
            True
        """
        freezing_points = {"water": 0}
        
        if substance not in freezing_points:
            raise ValueError(f"Freezing point for '{substance}' is not known")
        
        return self._celsius <= freezing_points[substance]

Common Patterns and Best Practices

Validation Methods

class EmailValidator:
    def __init__(self):
        self.valid_domains = {'gmail.com', 'yahoo.com', 'outlook.com'}
    
    def _is_valid_format(self, email):
        """Private helper method to check email format."""
        return '@' in email and '.' in email.split('@')[1]
    
    def _is_allowed_domain(self, email):
        """Private helper method to check domain."""
        domain = email.split('@')[1]
        return domain in self.valid_domains
    
    def validate(self, email):
        """Validate email address using helper methods."""
        if not isinstance(email, str):
            return False, "Email must be a string"
        
        if not self._is_valid_format(email):
            return False, "Invalid email format"
        
        if not self._is_allowed_domain(email):
            return False, "Domain not allowed"
        
        return True, "Valid email"

validator = EmailValidator()
is_valid, message = validator.validate("user@gmail.com")
print(f"Valid: {is_valid}, Message: {message}")

State Management

class GameCharacter:
    def __init__(self, name, health=100):
        self.name = name
        self.max_health = health
        self.health = health
        self.level = 1
        self.experience = 0
        self.is_alive = True
    
    def take_damage(self, damage):
        """Apply damage to character."""
        if not self.is_alive:
            return f"{self.name} is already defeated"
        
        self.health = max(0, self.health - damage)
        
        if self.health == 0:
            self.is_alive = False
            return f"{self.name} has been defeated!"
        
        return f"{self.name} takes {damage} damage. Health: {self.health}/{self.max_health}"
    
    def heal(self, amount):
        """Restore health to character."""
        if not self.is_alive:
            return f"Cannot heal {self.name} - character is defeated"
        
        old_health = self.health
        self.health = min(self.max_health, self.health + amount)
        healed = self.health - old_health
        
        return f"{self.name} healed for {healed} points. Health: {self.health}/{self.max_health}"
    
    def gain_experience(self, exp):
        """Add experience and handle level ups."""
        if not self.is_alive:
            return f"{self.name} cannot gain experience while defeated"
        
        self.experience += exp
        messages = [f"{self.name} gains {exp} experience"]
        
        # Check for level up (simplified: 100 exp per level)
        while self.experience >= self.level * 100:
            self.experience -= self.level * 100
            self.level += 1
            self.max_health += 20
            self.health = self.max_health  # Full heal on level up
            messages.append(f"Level up! {self.name} is now level {self.level}")
        
        return ". ".join(messages)

character = GameCharacter("Hero")
print(character.take_damage(30))        # Hero takes 30 damage
print(character.heal(10))               # Hero healed for 10 points
print(character.gain_experience(150))   # Level up occurs

Why Instance Methods Matter

Instance methods are essential because they:

  • Encapsulate behavior: Keep related functionality with the data it operates on
  • Maintain object state: Allow objects to modify their own attributes safely
  • Provide interfaces: Define how other code can interact with objects
  • Enable polymorphism: Different classes can implement the same method names differently
  • Support data validation: Control how object data is accessed and modified
  • Model real-world actions: Let objects perform actions that make sense for their type

As emphasized in the python-org README, instance methods are fundamental to object-oriented programming because they define what objects can do. They transform simple data containers into active entities that can:

  • Respond to requests: Through getter methods
  • Change their state: Through setter and action methods
  • Answer questions: Through query methods
  • Interact with other objects: Through methods that take other objects as parameters
  • Validate their own data: Through internal validation logic

Mastering instance methods - including the self parameter, method types, parameter handling, and error management - is essential for creating robust, maintainable object-oriented Python code. They provide the mechanism for objects to exhibit intelligent behavior and maintain their own integrity.