Skip to content

Latest commit

 

History

History
283 lines (219 loc) · 9.7 KB

File metadata and controls

283 lines (219 loc) · 9.7 KB

Avoiding Mutable Default Arguments in Python Functions

Slide 1: Understanding Mutable Default Arguments

Default arguments in Python functions that use mutable objects like lists or dictionaries can lead to unexpected behavior because these defaults are created once when the function is defined, not each time it's called. This fundamental behavior requires careful consideration during implementation.

# Problematic implementation with mutable default
def add_item(item, items=[]):
    items.append(item)
    return items

# Multiple calls demonstrate the issue
print(add_item(1))  # Output: [1]
print(add_item(2))  # Output: [1, 2] - Unexpected!
print(add_item(3))  # Output: [1, 2, 3] - Still accumulating!

Slide 2: Proper Implementation with None Default

Using None as a default value and initializing the mutable object inside the function ensures each function call starts with a fresh mutable object, preventing unexpected state preservation between calls.

# Correct implementation using None default
def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

# Multiple calls demonstrate correct behavior
print(add_item(1))  # Output: [1]
print(add_item(2))  # Output: [2]
print(add_item(3))  # Output: [3]

Slide 3: Real-world Example - User Preferences Handler

Implementing a user preferences system demonstrates how mutable defaults can affect application state management. This example shows a common pitfall in handling user settings with default values.

class UserPreferences:
    def __init__(self):
        self.preferences = {}
    
    # Problematic implementation
    def set_preferences(self, user_id, settings={}):
        settings['last_modified'] = '2024-03-15'
        self.preferences[user_id] = settings
        return self.preferences[user_id]

# Demo of the issue
prefs = UserPreferences()
print(prefs.set_preferences(1))  # {'last_modified': '2024-03-15'}
print(prefs.set_preferences(2))  # Same dict is modified!

Slide 4: Fixed User Preferences Implementation

The corrected implementation ensures each user gets their own fresh settings dictionary, preventing shared state between different users' preferences.

class UserPreferences:
    def __init__(self):
        self.preferences = {}
    
    def set_preferences(self, user_id, settings=None):
        if settings is None:
            settings = {}
        settings['last_modified'] = '2024-03-15'
        self.preferences[user_id] = settings.copy()  # Create a copy for safety
        return self.preferences[user_id]

# Demo of fixed implementation
prefs = UserPreferences()
print(prefs.set_preferences(1))  # {'last_modified': '2024-03-15'}
print(prefs.set_preferences(2))  # Fresh dict for user 2

Slide 5: Cache Implementation Anti-pattern

A common mistake in implementing caching mechanisms is using mutable default arguments to store cached results, which can lead to memory leaks and unexpected behavior in production systems.

# Problematic cache implementation
def compute_with_cache(n, cache={}):
    if n in cache:
        return cache[n]
    result = n * n  # Expensive computation
    cache[n] = result
    return result

# Cache persists between calls
print(compute_with_cache(2))  # 4
print(compute_with_cache(3))  # 9
print(compute_with_cache(2))  # Returns cached 4

Slide 6: Proper Cache Implementation

Implementing a cache system correctly requires careful consideration of scope and mutability. This example shows how to properly implement a cache mechanism using class-based design.

class ComputeCache:
    def __init__(self):
        self.cache = {}
    
    def compute(self, n):
        if n not in self.cache:
            self.cache[n] = n * n  # Expensive computation
        return self.cache[n]

# Proper cache usage
calculator = ComputeCache()
print(calculator.compute(2))  # 4
print(calculator.compute(3))  # 9
print(calculator.compute(2))  # Returns cached 4

Slide 7: Data Processing Pipeline Example

Processing data with default configurations demonstrates how mutable defaults can affect data pipeline results when handling multiple datasets with shared configuration parameters.

def process_dataset(data, config={}):
    config['processed'] = True
    return [x * config.get('multiplier', 1) for x in data]

# Problematic behavior
dataset1 = [1, 2, 3]
dataset2 = [4, 5, 6]
print(process_dataset(dataset1))  # [1, 2, 3]
config = {'multiplier': 2}
print(process_dataset(dataset2, config))  # [8, 10, 12]
print(process_dataset(dataset1))  # Unexpected behavior!

Slide 8: Corrected Data Processing Pipeline

A robust implementation of the data processing pipeline ensures configuration isolation between different dataset processing calls.

def process_dataset(data, config=None):
    if config is None:
        config = {}
    local_config = config.copy()  # Create local copy
    local_config['processed'] = True
    return [x * local_config.get('multiplier', 1) for x in data]

# Correct behavior
dataset1 = [1, 2, 3]
dataset2 = [4, 5, 6]
print(process_dataset(dataset1))  # [1, 2, 3]
config = {'multiplier': 2}
print(process_dataset(dataset2, config))  # [8, 10, 12]
print(process_dataset(dataset1))  # [1, 2, 3] - Correct!

Slide 9: Event Handler Implementation

Event handling systems often require default configurations for different event types. Improper implementation with mutable defaults can cause event cross-contamination.

class EventHandler:
    def handle_event(self, event_type, handlers=[]):
        handlers.append(f"Processed {event_type}")
        return handlers

# Problematic usage
handler = EventHandler()
print(handler.handle_event("click"))  # ['Processed click']
print(handler.handle_event("keypress"))  # ['Processed click', 'Processed keypress']

Slide 10: Corrected Event Handler Implementation

The improved event handler implementation ensures proper isolation of event processing chains and prevents cross-contamination between different event types through careful management of handler lists.

class EventHandler:
    def handle_event(self, event_type, handlers=None):
        if handlers is None:
            handlers = []
        local_handlers = handlers.copy()  # Create local copy
        local_handlers.append(f"Processed {event_type}")
        return local_handlers

# Correct usage
handler = EventHandler()
print(handler.handle_event("click"))  # ['Processed click']
print(handler.handle_event("keypress"))  # ['Processed keypress']

Slide 11: Database Connection Pool Implementation

Database connection pooling demonstrates a critical use case where mutable default arguments could lead to connection leaks and improper resource management in production environments.

# Problematic implementation
def get_db_connection(pool=[]): 
    if not pool:
        pool.append({"connection": "db_connection_1"})
    return pool[0]

# Connection persists unexpectedly
print(get_db_connection())  # {'connection': 'db_connection_1'}
print(get_db_connection())  # Same connection object

Slide 12: Proper Database Connection Pool

A robust connection pool implementation requires careful state management and proper handling of connection lifecycle, demonstrating correct usage of immutable defaults.

class DatabasePool:
    def __init__(self):
        self.pool = []
        
    def get_connection(self, config=None):
        if config is None:
            config = {"timeout": 30, "retry": 3}
        
        if not self.pool:
            connection = {
                "id": id({}),  # Unique connection ID
                "config": config.copy(),
                "created_at": "2024-03-15"
            }
            self.pool.append(connection)
        return self.pool[0]

# Proper usage
db_pool = DatabasePool()
print(db_pool.get_connection())  # Fresh connection
print(db_pool.get_connection({"timeout": 60}))  # New configuration

Slide 13: Machine Learning Parameter Grid Implementation

Machine learning hyperparameter management showcases how mutable defaults can affect model training when handling multiple parameter configurations across different training sessions.

# Problematic implementation
def create_parameter_grid(params={}):
    params.update({
        "learning_rate": [0.01, 0.001],
        "batch_size": [32, 64]
    })
    return params

# Parameters accumulate unexpectedly
print(create_parameter_grid())
print(create_parameter_grid({"epochs": [10, 20]}))  # Previous params remain

Slide 14: Corrected Parameter Grid Implementation

A proper implementation ensures parameter grids remain isolated between different training configurations, preventing parameter bleeding between experimental setups.

def create_parameter_grid(params=None):
    base_params = {
        "learning_rate": [0.01, 0.001],
        "batch_size": [32, 64]
    }
    
    if params is not None:
        combined_params = base_params.copy()
        combined_params.update(params)
        return combined_params
    return base_params.copy()

# Correct usage
print(create_parameter_grid())  # Base parameters only
print(create_parameter_grid({"epochs": [10, 20]}))  # Clean combination

Slide 15: Additional Resources