Skip to content

Latest commit

 

History

History
532 lines (400 loc) · 16.4 KB

File metadata and controls

532 lines (400 loc) · 16.4 KB

Shared vs Independent Lists in Python

Slide 1: Understanding List References in Python

List references in Python represent a fundamental concept where multiple variable names can point to the same underlying list object in memory, following Python's object-oriented nature where variables act as references rather than independent containers.

# Creating a shared list reference
original_list = [1, 2, 3]
shared_list = original_list  # Both variables reference same list

# Modifying through one reference affects both
original_list.append(4)
print(f"Original: {original_list}")  # Output: [1, 2, 3, 4]
print(f"Shared: {shared_list}")     # Output: [1, 2, 3, 4]

# Verify they point to same object
print(f"Same object: {original_list is shared_list}")  # Output: True

Slide 2: Creating Independent List Copies

To maintain data integrity and prevent unintended modifications, Python offers multiple methods to create independent copies of lists, each with different depth levels of copying the nested structures.

# Method 1: Slice copying
original = [1, [2, 3], 4]
slice_copy = original[:]

# Method 2: List constructor
constructor_copy = list(original)

# Method 3: copy() method
method_copy = original.copy()

# Modifying original doesn't affect copies
original[0] = 99
print(f"Original: {original}")      # Output: [99, [2, 3], 4]
print(f"Slice copy: {slice_copy}")  # Output: [1, [2, 3], 4]

Slide 3: Deep Copy Implementation

Deep copying ensures complete independence by recursively copying all nested objects, creating a fully independent data structure that can be modified without affecting the original list or its nested components.

from copy import deepcopy

# Creating nested structure
nested_list = [1, [2, 3, [4, 5]], 6]

# Creating deep copy
deep_copied = deepcopy(nested_list)

# Modifying nested element
nested_list[1][2][0] = 99

print(f"Original: {nested_list}")    # Output: [1, [2, 3, [99, 5]], 6]
print(f"Deep copy: {deep_copied}")   # Output: [1, [2, 3, [4, 5]], 6]

Slide 4: Memory Efficiency with Shared Lists

Memory management becomes crucial when working with large datasets, where shared lists can significantly reduce memory usage by allowing multiple views of the same data without duplicating the underlying storage.

import sys

# Create large list
large_list = list(range(1000000))

# Create reference and copy
shared_ref = large_list
independent_copy = large_list.copy()

# Compare memory usage
print(f"Original size: {sys.getsizeof(large_list)}")
print(f"Shared ref size: {sys.getsizeof(shared_ref)}")
print(f"Copy size: {sys.getsizeof(independent_copy)}")

Slide 5: List Modification Patterns

Understanding how different modification operations affect shared and independent lists is crucial for maintaining data integrity and preventing unexpected behavior in complex applications.

# Initialize lists
shared_data = [1, 2, 3]
reference = shared_data
independent = shared_data.copy()

# Different modification patterns
shared_data += [4]  # In-place modification
print(f"Reference affected: {reference}")  # [1, 2, 3, 4]

shared_data = shared_data + [5]  # New object assignment
print(f"Reference unchanged: {reference}")  # [1, 2, 3, 4]

independent.extend([6])
print(f"Original unaffected: {shared_data}")  # [1, 2, 3, 4]

Slide 6: Real-world Application: Data Processing Pipeline

In data processing pipelines, understanding list behavior is crucial when implementing transformation stages where each step may need either shared or independent data access for efficiency and correctness.

class DataPipeline:
    def __init__(self, data):
        self.master_data = data
        self.processed_data = None
        
    def preprocess(self, shared=True):
        # Choose between shared or independent processing
        self.processed_data = self.master_data if shared else self.master_data.copy()
        return self
        
    def transform(self):
        # Modifications affect master_data if shared
        for i in range(len(self.processed_data)):
            self.processed_data[i] = self.processed_data[i] * 2
        return self
    
# Example usage
data = [1, 2, 3, 4, 5]
pipe_shared = DataPipeline(data)
pipe_shared.preprocess(shared=True).transform()
print(f"Original data modified: {data}")  # [2, 4, 6, 8, 10]

data = [1, 2, 3, 4, 5]
pipe_independent = DataPipeline(data)
pipe_independent.preprocess(shared=False).transform()
print(f"Original data preserved: {data}")  # [1, 2, 3, 4, 5]

Slide 7: List Reference Tracking System

Implementation of a sophisticated tracking system that monitors list references and modifications, useful for debugging and understanding complex data flows in larger applications.

class ListTracker:
    def __init__(self):
        self._references = {}
        self._modifications = []
        
    def track(self, list_obj, name):
        id_obj = id(list_obj)
        if id_obj not in self._references:
            self._references[id_obj] = []
        self._references[id_obj].append(name)
        return list_obj
        
    def log_modification(self, list_obj, operation):
        id_obj = id(list_obj)
        affected_refs = self._references.get(id_obj, [])
        self._modifications.append(f"Operation '{operation}' affects: {affected_refs}")
        
    def get_history(self):
        return '\n'.join(self._modifications)

# Usage example
tracker = ListTracker()
original = tracker.track([1, 2, 3], "original")
reference = tracker.track(original, "reference")
independent = tracker.track(original.copy(), "independent")

original.append(4)
tracker.log_modification(original, "append(4)")
print(tracker.get_history())

Slide 8: Memory Optimization Techniques

Advanced memory optimization strategies for handling large-scale list operations while maintaining control over data sharing and independence.

import sys
from array import array

class MemoryOptimizedList:
    def __init__(self, data, shared=False):
        # Use array for memory efficiency
        self._data = array('i', data)
        self._shared = shared
        
    def get_view(self):
        return self._data if self._shared else array('i', self._data)
        
    def memory_usage(self):
        return sys.getsizeof(self._data)
    
    def __repr__(self):
        return f"MemoryOptimizedList({list(self._data)}, shared={self._shared})"

# Comparison
regular_list = list(range(1000000))
optimized = MemoryOptimizedList(range(1000000))

print(f"Regular list: {sys.getsizeof(regular_list)} bytes")
print(f"Optimized: {optimized.memory_usage()} bytes")

# Create views
shared_view = optimized.get_view()
independent_view = optimized.get_view()

Slide 9: Thread-Safe List Operations

Implementation of thread-safe list operations ensuring data consistency when shared lists are accessed concurrently in multi-threaded environments.

import threading
from typing import List
from dataclasses import dataclass

@dataclass
class ThreadSafeList:
    _data: List
    _lock: threading.Lock = threading.Lock()
    
    def modify(self, index: int, value: any) -> None:
        with self._lock:
            self._data[index] = value
    
    def safe_copy(self) -> List:
        with self._lock:
            return self._data.copy()
    
    def __str__(self) -> str:
        with self._lock:
            return str(self._data)

# Usage in threaded environment
def worker(safe_list: ThreadSafeList, index: int):
    safe_list.modify(index, index * 2)

# Example
shared_list = ThreadSafeList([0] * 5)
threads = [
    threading.Thread(target=worker, args=(shared_list, i))
    for i in range(5)
]

for t in threads:
    t.start()
for t in threads:
    t.join()

print(f"Result: {shared_list}")

Slide 10: Performance Analysis of List Operations

Comprehensive benchmarking system for comparing performance characteristics of shared versus independent list operations across different scales and operation types.

import time
import numpy as np
from typing import Callable, List

class ListPerformanceAnalyzer:
    def __init__(self, sizes: List[int]):
        self.sizes = sizes
        self.results = {}
        
    def benchmark(self, operation: Callable, name: str):
        times = []
        for size in self.sizes:
            data = list(range(size))
            
            start = time.perf_counter()
            operation(data)
            end = time.perf_counter()
            
            times.append(end - start)
        self.results[name] = times
        
    def compare_operations(self):
        for name, times in self.results.items():
            print(f"\n{name}:")
            print(f"Average time: {np.mean(times):.6f} seconds")
            print(f"Std deviation: {np.std(times):.6f} seconds")

# Example usage
analyzer = ListPerformanceAnalyzer([1000, 10000, 100000])

# Test operations
analyzer.benchmark(
    lambda x: x.copy(), 
    "Independent copy"
)
analyzer.benchmark(
    lambda x: x[:], 
    "Slice copy"
)
analyzer.benchmark(
    lambda x: x, 
    "Shared reference"
)

analyzer.compare_operations()

Slide 11: Context-Aware List Management

Implementation of a smart list container that automatically decides between shared and independent operations based on usage context and performance requirements.

class ContextAwareList:
    def __init__(self, data: list):
        self._data = data
        self._access_count = 0
        self._modification_count = 0
        self._sharing_threshold = 5
        
    def get_view(self, context: str = "read"):
        self._access_count += 1
        
        if context == "modify":
            self._modification_count += 1
            
        # Decide sharing strategy
        should_share = (
            self._access_count > self._sharing_threshold and 
            self._modification_count / self._access_count < 0.3
        )
        
        return self._data if should_share else self._data.copy()
    
    def get_stats(self):
        return {
            "accesses": self._access_count,
            "modifications": self._modification_count,
            "sharing_ratio": self._modification_count / max(1, self._access_count)
        }

# Usage example
data = list(range(1000))
smart_list = ContextAwareList(data)

# Simulate different access patterns
for _ in range(10):
    view = smart_list.get_view("read")
    
for _ in range(2):
    view = smart_list.get_view("modify")
    
print(f"Usage statistics: {smart_list.get_stats()}")

Slide 12: Memory Leak Prevention System

Advanced system for tracking and preventing memory leaks in applications using shared lists, particularly useful in long-running applications with dynamic list management.

import weakref
from typing import Dict, Set
import gc

class ListLeakDetector:
    def __init__(self):
        self._tracked_lists: Dict[int, Set[weakref.ref]] = {}
        
    def register(self, lst: list, owner: str):
        list_id = id(lst)
        if list_id not in self._tracked_lists:
            self._tracked_lists[list_id] = set()
            
        # Create weak reference to avoid circular references
        ref = weakref.ref(lst)
        self._tracked_lists[list_id].add((ref, owner))
        
    def check_leaks(self):
        leaked_lists = []
        for list_id, references in self._tracked_lists.items():
            active_refs = [(ref, owner) for ref, owner in references if ref() is not None]
            if len(active_refs) > 1:
                leaked_lists.append((list_id, [owner for _, owner in active_refs]))
                
        return leaked_lists

# Example usage
detector = ListLeakDetector()

def potential_leak_function():
    data = [1, 2, 3]
    detector.register(data, "function_scope")
    return data

shared_list = potential_leak_function()
detector.register(shared_list, "global_scope")

# Force garbage collection
gc.collect()
print(f"Detected leaks: {detector.check_leaks()}")

Slide 13: Real-world Application: Data Pipeline with Change Tracking

Implementation of a sophisticated data processing pipeline that maintains history of transformations while efficiently managing memory through strategic sharing and copying of data.

from dataclasses import dataclass
from typing import List, Dict, Any, Optional
import hashlib
import json

@dataclass
class DataState:
    data: List
    hash: str
    parent_hash: Optional[str] = None

class TrackedPipeline:
    def __init__(self):
        self.states: Dict[str, DataState] = {}
        
    def _compute_hash(self, data: List) -> str:
        return hashlib.md5(json.dumps(data).encode()).hexdigest()
        
    def add_state(self, data: List, parent_hash: Optional[str] = None) -> str:
        current_hash = self._compute_hash(data)
        self.states[current_hash] = DataState(
            data=data,
            hash=current_hash,
            parent_hash=parent_hash
        )
        return current_hash
        
    def transform(self, data_hash: str, operation: callable, shared: bool = False) -> str:
        current_state = self.states[data_hash]
        input_data = current_state.data if shared else current_state.data.copy()
        transformed_data = operation(input_data)
        return self.add_state(transformed_data, parent_hash=data_hash)
    
    def get_lineage(self, state_hash: str) -> List[List]:
        lineage = []
        current_hash = state_hash
        
        while current_hash:
            state = self.states[current_hash]
            lineage.append(state.data)
            current_hash = state.parent_hash
            
        return lineage[::-1]

# Example usage
pipeline = TrackedPipeline()

# Initial data
initial_data = [1, 2, 3, 4, 5]
state1 = pipeline.add_state(initial_data)

# Apply transformations
state2 = pipeline.transform(state1, lambda x: [i * 2 for i in x])
state3 = pipeline.transform(state2, lambda x: [i + 1 for i in x])

# Get transformation history
lineage = pipeline.get_lineage(state3)
print("Transformation lineage:")
for i, state in enumerate(lineage):
    print(f"Stage {i}: {state}")

Slide 14: Advanced Memory Optimization Patterns

Implementation of sophisticated memory optimization patterns for handling large-scale list operations while maintaining performance and memory efficiency.

from typing import TypeVar, Generic, List, Optional
import sys
import numpy as np

T = TypeVar('T')

class OptimizedListContainer(Generic[T]):
    def __init__(self, threshold: int = 1000):
        self._data: Optional[List[T]] = None
        self._numpy_data: Optional[np.ndarray] = None
        self._threshold = threshold
        self._shared_count = 0
        
    def _convert_to_numpy(self):
        if self._data is not None and len(self._data) >= self._threshold:
            self._numpy_data = np.array(self._data)
            self._data = None
            
    def _convert_to_list(self):
        if self._numpy_data is not None and self._shared_count == 0:
            self._data = self._numpy_data.tolist()
            self._numpy_data = None
    
    def set_data(self, data: List[T]):
        self._data = data
        self._convert_to_numpy()
        
    def get_view(self, shared: bool = True) -> List[T]:
        if shared:
            self._shared_count += 1
            return self._data if self._data is not None else self._numpy_data.tolist()
        else:
            return (self._data.copy() if self._data is not None 
                   else self._numpy_data.tolist())
    
    def release_shared(self):
        self._shared_count = max(0, self._shared_count - 1)
        self._convert_to_list()
        
    def memory_usage(self) -> int:
        if self._data is not None:
            return sys.getsizeof(self._data)
        return self._numpy_data.nbytes if self._numpy_data is not None else 0

# Usage example
container = OptimizedListContainer[int](threshold=100)
data = list(range(1000))
container.set_data(data)

view1 = container.get_view(shared=True)
view2 = container.get_view(shared=False)

print(f"Memory usage: {container.memory_usage()} bytes")
container.release_shared()

Slide 15: Additional Resources