Skip to content

Latest commit

 

History

History
491 lines (376 loc) · 14.6 KB

File metadata and controls

491 lines (376 loc) · 14.6 KB

Python's Dynamic Variable Declaration

Slide 1: Variable Declaration Basics in Python

Python's dynamic typing system allows variables to be created without explicit type declarations, unlike statically typed languages. This fundamental difference enables rapid development and flexibility in data manipulation, while still maintaining type safety through runtime type checking.

# Dynamic typing demonstration
x = 42                  # Integer
print(f"Type: {type(x)}, Value: {x}")

x = "Hello"            # Same variable, now a string
print(f"Type: {type(x)}, Value: {x}")

x = [1, 2, 3]          # Same variable, now a list
print(f"Type: {type(x)}, Value: {x}")

# Output:
# Type: <class 'int'>, Value: 42
# Type: <class 'str'>, Value: Hello
# Type: <class 'list'>, Value: [1, 2, 3]

Slide 2: Type Inference and Memory Management

Python's interpreter automatically handles memory allocation and type inference, determining the appropriate data type based on the assigned value. This process occurs dynamically at runtime, with Python managing the memory references and garbage collection.

# Memory and type inference demonstration
import sys

# Integer object
number = 42
print(f"Size in memory: {sys.getsizeof(number)} bytes")

# String object
text = "Python"
print(f"Size in memory: {sys.getsizeof(text)} bytes")

# List with mixed types
mixed = [1, "two", 3.0]
print(f"Container type: {type(mixed)}")
print(f"Element types: {[type(x).__name__ for x in mixed]}")

# Output:
# Size in memory: 28 bytes
# Size in memory: 55 bytes
# Container type: <class 'list'>
# Element types: ['int', 'str', 'float']

Slide 3: Variable Reference Mechanics

Understanding Python's reference mechanism is crucial for effective programming. Variables act as references to objects in memory, rather than containers holding values directly. This behavior influences how data is copied and modified.

# Reference behavior demonstration
list_a = [1, 2, 3]
list_b = list_a        # Creates a reference to the same object

print(f"Initial lists: A={list_a}, B={list_b}")
print(f"Same object? {list_a is list_b}")

list_b.append(4)
print(f"After modification: A={list_a}, B={list_b}")

# Creating a true copy
list_c = list_a.copy()
list_c.append(5)
print(f"Original: {list_a}")
print(f"True copy: {list_c}")

# Output:
# Initial lists: A=[1, 2, 3], B=[1, 2, 3]
# Same object? True
# After modification: A=[1, 2, 3, 4], B=[1, 2, 3, 4]
# Original: [1, 2, 3, 4]
# True copy: [1, 2, 3, 4, 5]

Slide 4: Type Hinting in Modern Python

While Python remains dynamically typed, type hints introduced in Python 3.5+ provide optional static type checking and improved code documentation. This feature bridges the gap between dynamic and static typing systems.

from typing import List, Dict, Optional

def process_data(items: List[int], 
                mapping: Dict[str, float], 
                threshold: Optional[float] = None) -> List[float]:
    """
    Example of type hints in function signatures
    """
    result: List[float] = []
    for item in items:
        if str(item) in mapping:
            value = mapping[str(item)]
            if threshold is None or value > threshold:
                result.append(value)
    return result

# Usage example
numbers = [1, 2, 3, 4]
conversion = {"1": 1.5, "2": 2.5, "3": 3.5}
result = process_data(numbers, conversion, 2.0)
print(f"Processed data: {result}")

# Output:
# Processed data: [2.5, 3.5]

Slide 5: Memory Optimization with slots

Python's dynamic nature usually stores instance attributes in a dictionary, consuming extra memory. The slots declaration optimizes memory usage by restricting attributes to a fixed set, particularly useful in classes with numerous instances.

import sys

# Regular class
class RegularPoint:
    def __init__(self, x, y):
        self.x = x
        self.y = y

# Optimized class with __slots__
class OptimizedPoint:
    __slots__ = ['x', 'y']
    def __init__(self, x, y):
        self.x = x
        self.y = y

# Memory comparison
regular = RegularPoint(1, 2)
optimized = OptimizedPoint(1, 2)

print(f"Regular instance size: {sys.getsizeof(regular.__dict__)} bytes")
print(f"Optimized instance size: {sys.getsizeof(optimized)} bytes")

# Output:
# Regular instance size: 232 bytes
# Optimized instance size: 48 bytes

Slide 6: Dynamic Attribute Management

Python's dynamic nature extends to attribute management, allowing runtime modification of class and instance attributes. This flexibility enables metaprogramming and dynamic behavior adaptation, though it requires careful handling to maintain code clarity.

class DynamicClass:
    def __init__(self):
        self.initial = "First attribute"
    
    def __getattr__(self, name):
        # Called when attribute lookup fails
        print(f"Attempting to access undefined attribute: {name}")
        return None
    
    def __setattr__(self, name, value):
        # Called when setting any attribute
        print(f"Setting attribute {name} = {value}")
        super().__setattr__(name, value)

obj = DynamicClass()
print(obj.initial)           # Existing attribute
print(obj.undefined)         # Triggers __getattr__
obj.new_attr = "Dynamic"     # Triggers __setattr__

# Output:
# Setting attribute initial = First attribute
# First attribute
# Attempting to access undefined attribute: undefined
# None
# Setting attribute new_attr = Dynamic

Slide 7: Context-Aware Variable Scope Management

Python's variable scope rules affect how names are resolved in nested contexts. Understanding scope resolution is crucial for avoiding common pitfalls in variable declaration and access patterns.

def demonstrate_scope():
    global_var = "Global"
    
    def outer_function():
        outer_var = "Outer"
        
        def inner_function():
            nonlocal outer_var
            inner_var = "Inner"
            outer_var = "Modified Outer"
            
            print(f"Inner scope - inner_var: {inner_var}")
            print(f"Inner scope - outer_var: {outer_var}")
            print(f"Inner scope - global_var: {global_var}")
            
        print(f"Outer scope - before: {outer_var}")
        inner_function()
        print(f"Outer scope - after: {outer_var}")
    
    outer_function()

demonstrate_scope()

# Output:
# Outer scope - before: Outer
# Inner scope - inner_var: Inner
# Inner scope - outer_var: Modified Outer
# Inner scope - global_var: Global
# Outer scope - after: Modified Outer

Slide 8: Real-world Application: Dynamic Data Processing Pipeline

In real-world scenarios, Python's flexible variable declaration enables creation of dynamic data processing pipelines. This example demonstrates a configurable pipeline for processing sensor data with runtime-defined transformations.

class DataProcessor:
    def __init__(self):
        self.transformations = {}
        self.data = None
    
    def register_transformation(self, name, func):
        self.transformations[name] = func
    
    def process(self, data, pipeline):
        self.data = data
        for step in pipeline:
            if step in self.transformations:
                self.data = self.transformations[step](self.data)
        return self.data

# Example usage with sensor data
import numpy as np

# Create processor instance
processor = DataProcessor()

# Register transformations dynamically
processor.register_transformation('normalize', 
    lambda x: (x - np.mean(x)) / np.std(x))
processor.register_transformation('threshold', 
    lambda x: np.clip(x, -2, 2))

# Sample data and processing
sensor_data = np.random.normal(loc=10, scale=5, size=1000)
pipeline = ['normalize', 'threshold']
result = processor.process(sensor_data, pipeline)

print(f"Original stats - Mean: {np.mean(sensor_data):.2f}, Std: {np.std(sensor_data):.2f}")
print(f"Processed stats - Mean: {np.mean(result):.2f}, Std: {np.std(result):.2f}")

# Output:
# Original stats - Mean: 9.98, Std: 4.95
# Processed stats - Mean: 0.00, Std: 0.84

Slide 9: Memory Management and Garbage Collection

Python's garbage collector automatically manages memory allocation and deallocation. Understanding how variables are created and destroyed helps in optimizing memory usage and preventing memory leaks.

import gc
import weakref
import sys

class MemoryTest:
    def __init__(self, name):
        self.name = name
        print(f"Created {self.name}")
    
    def __del__(self):
        print(f"Destroyed {self.name}")

def demonstrate_memory_management():
    # Strong reference
    obj1 = MemoryTest("Object 1")
    
    # Weak reference
    obj2 = MemoryTest("Object 2")
    weak_ref = weakref.ref(obj2)
    
    # Force garbage collection
    print("\nGarbage collection stats before:")
    print(gc.get_count())
    
    # Delete strong reference
    del obj2
    print("\nAfter deleting obj2:")
    print(f"Weak reference alive? {weak_ref() is not None}")
    
    # Force garbage collection
    gc.collect()
    print("\nAfter garbage collection:")
    print(f"Weak reference alive? {weak_ref() is not None}")

demonstrate_memory_management()

# Output:
# Created Object 1
# Created Object 2
# Garbage collection stats before: (98, 7, 3)
# After deleting obj2:
# Destroyed Object 2
# Weak reference alive? False
# After garbage collection:
# Weak reference alive? False

Slide 10: Variable State Management in Concurrency

Python's variable handling in concurrent environments requires special consideration. This example demonstrates thread-safe variable management using synchronization primitives and thread-local storage.

import threading
import queue
import time
from threading import local

class ThreadSafeCounter:
    def __init__(self):
        self._lock = threading.Lock()
        self._value = 0
        self.thread_local = local()
        
    def increment(self):
        with self._lock:
            self._value += 1
            self.thread_local.last_update = time.time()
            return self._value
            
    def get_value(self):
        return self._value

def worker(counter, worker_id, iterations):
    for _ in range(iterations):
        value = counter.increment()
        print(f"Worker {worker_id}: Counter = {value}")
        time.sleep(0.1)

# Create and run threads
counter = ThreadSafeCounter()
threads = []
for i in range(3):
    t = threading.Thread(target=worker, args=(counter, i, 3))
    threads.append(t)
    t.start()

for t in threads:
    t.join()

print(f"Final counter value: {counter.get_value()}")

# Output:
# Worker 0: Counter = 1
# Worker 1: Counter = 2
# Worker 2: Counter = 3
# Worker 0: Counter = 4
# Worker 1: Counter = 5
# Worker 2: Counter = 6
# Worker 0: Counter = 7
# Worker 1: Counter = 8
# Worker 2: Counter = 9
# Final counter value: 9

Slide 11: Advanced Type Annotations with Generic Types

Python's type system supports generic types and type variables, enabling creation of reusable, type-safe components while maintaining dynamic flexibility.

from typing import TypeVar, Generic, List, Optional
from dataclasses import dataclass

T = TypeVar('T')

@dataclass
class Result(Generic[T]):
    value: Optional[T]
    error: Optional[str] = None
    
    @property
    def is_success(self) -> bool:
        return self.error is None

class GenericProcessor(Generic[T]):
    def __init__(self, initial: T):
        self.data: T = initial
        self.history: List[T] = [initial]
    
    def process(self, transformer: callable) -> Result[T]:
        try:
            result = transformer(self.data)
            self.data = result
            self.history.append(result)
            return Result(value=result)
        except Exception as e:
            return Result(value=None, error=str(e))

# Usage example
def double_number(x: int) -> int:
    return x * 2

processor = GenericProcessor[int](5)
result1 = processor.process(double_number)
result2 = processor.process(lambda x: x + 3)

print(f"Result 1: {result1}")
print(f"Result 2: {result2}")
print(f"History: {processor.history}")

# Output:
# Result 1: Result(value=10, error=None)
# Result 2: Result(value=13, error=None)
# History: [5, 10, 13]

Slide 12: Real-world Application: Dynamic Configuration System

This implementation showcases a production-ready configuration system utilizing Python's dynamic variable handling for flexible runtime configuration management.

from typing import Any, Dict, Optional
import json
from pathlib import Path

class DynamicConfig:
    def __init__(self):
        self._config: Dict[str, Any] = {}
        self._watchers: Dict[str, list] = {}
        
    def __getattr__(self, name: str) -> Any:
        if name in self._config:
            return self._config[name]
        raise AttributeError(f"Configuration '{name}' not found")
    
    def update(self, key: str, value: Any) -> None:
        old_value = self._config.get(key)
        self._config[key] = value
        
        if key in self._watchers:
            for callback in self._watchers[key]:
                callback(old_value, value)
    
    def watch(self, key: str, callback: callable) -> None:
        if key not in self._watchers:
            self._watchers[key] = []
        self._watchers[key].append(callback)
    
    def load_from_file(self, filepath: Path) -> None:
        with open(filepath, 'r') as f:
            new_config = json.load(f)
            for key, value in new_config.items():
                self.update(key, value)

# Example usage
def config_changed(old_value: Optional[Any], new_value: Any) -> None:
    print(f"Config changed: {old_value} -> {new_value}")

config = DynamicConfig()
config.watch('debug_mode', config_changed)

config.update('debug_mode', True)
config.update('max_connections', 100)

print(f"Debug mode: {config.debug_mode}")
print(f"Max connections: {config.max_connections}")

# Save configuration
with open('config.json', 'w') as f:
    json.dump({
        'debug_mode': False,
        'max_connections': 200
    }, f)

# Load new configuration
config.load_from_file(Path('config.json'))

# Output:
# Config changed: None -> True
# Debug mode: True
# Max connections: 100
# Config changed: True -> False

Slide 13: Additional Resources