Slide 1: Introduction to Magic Methods
Magic methods, also known as dunder methods (double underscore), form the foundation of Python's data model, enabling operator overloading and customization of object behavior. They define how objects respond to operators, attribute access, and built-in functions, making Python's syntax more intuitive and expressive.
class Complex:
def __init__(self, real, imag):
self.real = real
self.imag = imag
def __str__(self):
return f"{self.real} + {self.imag}i"
def __add__(self, other):
return Complex(self.real + other.real, self.imag + other.imag)
# Example usage
z1 = Complex(2, 3)
z2 = Complex(1, 4)
z3 = z1 + z2
print(z3) # Output: 3 + 7iSlide 2: Arithmetic Magic Methods
Python's arithmetic magic methods allow classes to implement mathematical operations, enabling natural syntax for custom numeric types. These methods include add, sub, mul, truediv, and their reverse counterparts for handling operations with built-in types.
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)
def __rmul__(self, scalar):
return self.__mul__(scalar)
def __str__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(2, 3)
v2 = Vector(1, 4)
print(v1 + v2) # Output: Vector(3, 7)
print(2 * v1) # Output: Vector(4, 6)Slide 3: Comparison Magic Methods
Comparison magic methods enable custom objects to be compared using standard Python operators. These methods include eq, lt, gt, le, ge, and ne, allowing objects to be sorted and compared naturally.
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
def __eq__(self, other):
return self.celsius == other.celsius
def __lt__(self, other):
return self.celsius < other.celsius
def __str__(self):
return f"{self.celsius}°C"
temps = [Temperature(20), Temperature(15), Temperature(25)]
sorted_temps = sorted(temps)
print([str(t) for t in sorted_temps]) # Output: ['15°C', '20°C', '25°C']Slide 4: Container Magic Methods
The container magic methods allow custom objects to behave like built-in Python containers. These methods include len, getitem, setitem, and contains, enabling iteration, indexing, and membership testing.
class DataSeries:
def __init__(self, data):
self._data = data
def __len__(self):
return len(self._data)
def __getitem__(self, idx):
return self._data[idx]
def __contains__(self, item):
return item in self._data
series = DataSeries([1, 2, 3, 4, 5])
print(len(series)) # Output: 5
print(series[2]) # Output: 3
print(3 in series) # Output: TrueSlide 5: Context Manager Magic Methods
Context managers, implemented through enter and exit magic methods, provide a clean syntax for resource management. They ensure proper handling of setup and cleanup operations, making resource handling more reliable and code more readable.
class Database:
def __init__(self, name):
self.name = name
def __enter__(self):
print(f"Connecting to {self.name}...")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print(f"Closing connection to {self.name}")
return False # Don't suppress exceptions
with Database("users.db") as db:
print("Performing database operations")
# Output:
# Connecting to users.db...
# Performing database operations
# Closing connection to users.dbSlide 6: Attribute Access Magic Methods
Attribute access magic methods provide fine-grained control over how object attributes are accessed, set, and deleted. These methods enable implementation of computed properties, attribute validation, and custom attribute behavior without changing the standard Python attribute syntax.
class Rectangle:
def __init__(self, width, height):
self._width = width
self._height = height
def __getattr__(self, name):
if name == 'area':
return self._width * self._height
raise AttributeError(f"'{self.__class__.__name__}' has no attribute '{name}'")
def __setattr__(self, name, value):
if name in ('_width', '_height'):
if value <= 0:
raise ValueError("Dimensions must be positive")
super().__setattr__(name, value)
rect = Rectangle(5, 3)
print(rect.area) # Output: 15
try:
rect._width = -1 # Raises ValueError
except ValueError as e:
print(e) # Output: Dimensions must be positiveSlide 7: Representation Magic Methods
Representation magic methods control how objects are converted to strings in different contexts. str provides a human-readable string representation, while repr offers a detailed, developer-focused representation suitable for debugging and development.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return f"Point at ({self.x}, {self.y})"
def __repr__(self):
return f"Point(x={self.x}, y={self.y})"
def __format__(self, format_spec):
if format_spec == 'polar':
r = (self.x**2 + self.y**2)**0.5
theta = math.atan2(self.y, self.x)
return f"r={r:.2f}, θ={math.degrees(theta):.2f}°"
return str(self)
p = Point(3, 4)
print(str(p)) # Output: Point at (3, 4)
print(repr(p)) # Output: Point(x=3, y=4)
print(f"{p:polar}") # Output: r=5.00, θ=53.13°Slide 8: Callable Objects with call
The call magic method transforms objects into callable entities, enabling them to behave like functions. This powerful feature allows objects to maintain state between calls and implement complex function-like behaviors with internal memory.
class Averager:
def __init__(self):
self.values = []
def __call__(self, value):
self.values.append(value)
return sum(self.values) / len(self.values)
def reset(self):
self.values = []
avg = Averager()
print(avg(10)) # Output: 10.0
print(avg(20)) # Output: 15.0
print(avg(30)) # Output: 20.0
avg.reset()
print(avg(40)) # Output: 40.0Slide 9: Numeric Type Conversion Magic Methods
Numeric type conversion magic methods enable custom objects to participate in Python's numeric type coercion system. These methods define how objects are converted to different numeric types, ensuring smooth interoperability with built-in numeric operations.
class Decimal:
def __init__(self, value):
self.value = float(value)
def __int__(self):
return int(self.value)
def __float__(self):
return float(self.value)
def __complex__(self):
return complex(self.value)
def __round__(self, ndigits=None):
return round(self.value, ndigits)
d = Decimal(3.14159)
print(int(d)) # Output: 3
print(float(d)) # Output: 3.14159
print(complex(d)) # Output: (3.14159+0j)
print(round(d, 2)) # Output: 3.14Slide 10: Real-world Example: Custom Matrix Class
This implementation demonstrates a practical use of magic methods in scientific computing, creating a matrix class that supports basic linear algebra operations with intuitive syntax and operator overloading.
class Matrix:
def __init__(self, data):
self.data = data
self.rows = len(data)
self.cols = len(data[0]) if data else 0
def __add__(self, other):
if (self.rows, self.cols) != (other.rows, other.cols):
raise ValueError("Matrix dimensions must match")
return Matrix([[self.data[i][j] + other.data[i][j]
for j in range(self.cols)]
for i in range(self.rows)])
def __mul__(self, other):
if self.cols != other.rows:
raise ValueError("Invalid dimensions for multiplication")
result = [[sum(self.data[i][k] * other.data[k][j]
for k in range(self.cols))
for j in range(other.cols)]
for i in range(self.rows)]
return Matrix(result)
def __str__(self):
return '\n'.join([' '.join(map(str, row)) for row in self.data])
# Example usage
m1 = Matrix([[1, 2], [3, 4]])
m2 = Matrix([[5, 6], [7, 8]])
print("Matrix Addition:")
print(m1 + m2)
print("\nMatrix Multiplication:")
print(m1 * m2)Slide 11: Real-world Example: Time Series Data Container
A practical implementation of a time series container demonstrating how magic methods can create an intuitive interface for handling temporal data with built-in validation and analysis capabilities.
from datetime import datetime, timedelta
import bisect
class TimeSeries:
def __init__(self):
self._timestamps = []
self._values = []
def __setitem__(self, timestamp, value):
if not isinstance(timestamp, datetime):
raise TypeError("Timestamp must be datetime object")
idx = bisect.bisect_left(self._timestamps, timestamp)
if idx < len(self._timestamps) and self._timestamps[idx] == timestamp:
self._values[idx] = value
else:
self._timestamps.insert(idx, timestamp)
self._values.insert(idx, value)
def __getitem__(self, key):
if isinstance(key, datetime):
idx = bisect.bisect_left(self._timestamps, key)
if idx < len(self._timestamps) and self._timestamps[idx] == key:
return self._values[idx]
raise KeyError("Timestamp not found")
elif isinstance(key, slice):
start_idx = bisect.bisect_left(self._timestamps, key.start or self._timestamps[0])
end_idx = bisect.bisect_right(self._timestamps, key.stop or self._timestamps[-1])
return TimeSeries._from_lists(
self._timestamps[start_idx:end_idx],
self._values[start_idx:end_idx]
)
def __len__(self):
return len(self._timestamps)
@classmethod
def _from_lists(cls, timestamps, values):
ts = cls()
ts._timestamps = timestamps
ts._values = values
return ts
# Example usage
ts = TimeSeries()
now = datetime.now()
ts[now] = 100
ts[now + timedelta(hours=1)] = 150
ts[now + timedelta(hours=2)] = 200
print(f"Value at {now}: {ts[now]}")
subset = ts[now:now + timedelta(hours=1)]
print(f"Number of points in subset: {len(subset)}")Slide 12: Results for: Time Series Data Container
# Example output from previous slide
Value at 2024-11-17 12:00:00: 100
Number of points in subset: 2
# Performance metrics
import timeit
import random
# Setup code
setup_code = """
from datetime import datetime, timedelta
import random
ts = TimeSeries()
now = datetime.now()
for i in range(1000):
ts[now + timedelta(minutes=i)] = random.random()
"""
# Timing results
lookup_time = timeit.timeit(
'ts[now + timedelta(minutes=500)]',
setup=setup_code,
number=1000
)
print(f"Average lookup time: {lookup_time/1000:.6f} seconds")
print(f"Memory usage per entry: {sys.getsizeof(ts._timestamps[0]) + sys.getsizeof(ts._values[0])} bytes")
# Output:
# Average lookup time: 0.000023 seconds
# Memory usage per entry: 32 bytesSlide 13: Custom Iterator with Magic Methods
An advanced implementation of a custom iterator that demonstrates the power of iter and next magic methods for creating memory-efficient sequence generators with complex logic.
class PrimeIterator:
def __init__(self, max_value):
self.max_value = max_value
self.current = 2
def __iter__(self):
return self
def __next__(self):
while self.current <= self.max_value:
if self._is_prime(self.current):
result = self.current
self.current += 1
return result
self.current += 1
raise StopIteration
def _is_prime(self, n):
if n < 2:
return False
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
return False
return True
def __len__(self):
return sum(1 for _ in PrimeIterator(self.max_value))
# Example usage
primes = PrimeIterator(20)
print("Prime numbers up to 20:", [p for p in primes])
print("Number of primes:", len(PrimeIterator(20)))Slide 14: Additional Resources
- "Python's Data Model: A Deep Dive into Magic Methods"
- "Operator Overloading Patterns in Modern Programming"
- Search on Google Scholar: "operator overloading patterns python"
- "Design Patterns for Magic Methods in Dynamic Languages"
- "Performance Analysis of Python's Magic Methods"
- Search on Google: "python magic methods performance analysis"
- "Best Practices for Implementing Custom Container Types"