Python Advanced Class Design: Deep Object Manipulation Techniques
Extending Built-in Immutable Types with Custom Instantiation
Problem Scenario
We need to create a custom tuple variant that filters an iterable to retain only positive integer values:
FilteredTuple([1, -1, 'abc', 6, ['x', 'y'], 3]) => (1, 6, 3)
Solution
Override the __new__ method to intercept object creation before initialization:
class FilteredTuple(tuple):
def __new__(cls, data):
filtered = (x for x in data if isinstance(x, int) and x > 0)
return super().__new__(cls, filtered)
result = FilteredTuple([1, -1, 'abc', 6, ['x', 'y'], 3])
print(result) # Output: (1, 6, 3)
Memory Optimization for High-Volume Instances
Problem Scenario
In a multiplayer on line game, each online player requires a Player instance on the server. With millions of concurrent users, memory consumption becomes critical.
Solution
Declare __slots__ to pre-allocate instance attributes, eliminating the dynamic __dict__ dictionary:
class PlayerBasic:
def __init__(self, uid, name, level):
self.uid = uid
self.name = name
self.level = level
class PlayerOptimized:
__slots__ = ['uid', 'name', 'level']
def __init__(self, uid, name, level):
self.uid = uid
self.name = name
self.level = level
import sys
basic = PlayerBasic('001', 'Alice', 30)
optimized = PlayerOptimized('001', 'Alice', 30)
extra_attrs = set(dir(basic)) - set(dir(optimized))
print(f"Extra attributes in basic: {extra_attrs}")
# Output: {'__dict__', '__weakref__'}
print(f"Size of __dict__: {sys.getsizeof(basic.__dict__)} bytes")
# Output: 864 bytes
# Dynamic attribute addition works with basic class
basic.new_attribute = "test" # Success
# Dynamic addition fails with __slots__ class
# optimized.new_attribute = "test" # AttributeError
Implementing Managed Object Attributes
Problem Scenario
Direct attribute access lacks validation, while method call are verbose:
# Verbose approach
circle.get_radius()
circle.set_radius(5.0)
# Desired concise approach
circle.radius
circle.radius = 5.0
Solution
Use the property decorator to create managed attributes:
import math
class Circle:
def __init__(self, radius):
self.radius = radius
def get_radius(self):
return round(self.radius, 1)
def set_radius(self, value):
if not isinstance(value, (int, float)):
raise TypeError('radius must be numeric')
self.radius = value
@property
def area(self):
return self.radius ** 2 * math.pi
@area.setter
def area(self, value):
self.radius = math.sqrt(value / math.pi)
# Alternative: property with getter/setter functions
radius_prop = property(get_radius, set_radius)
circle = Circle(5.712)
circle.area = 99.88
print(f"Area: {circle.area}") # 99.88
print(f"Radius: {circle.radius_prop}") # 5.6
print(f"Via getter: {circle.get_radius()}") # 5.6
Enabling Comparison Operations for Classes
Solution
Implement comparison dunder methods or use @total_ordering decorator to reduce boilerplate:
from functools import total_ordering
from abc import ABC, abstractmethod
@total_ordering
class Shape(ABC):
@abstractmethod
def area(self):
pass
def __lt__(self, other):
print(f'__lt__ called: {self} vs {other}')
return self.area() < other.area()
def __eq__(self, other):
return self.area() == other.area()
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def __str__(self):
return f'Rectangle({self.width}, {self.height})'
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return self.radius ** 2 * 3.14159
rect1 = Rectangle(6, 9) # Area: 54
rect2 = Rectangle(7, 8) Area: 56
circle = Circle(8) # Area: ~201.06
print(rect1 < circle) # True
print(circle > rect2) # True
print(rect1 == Rectangle(6, 9)) # True
Type Checking with Descriptors
Solution
Implement the descriptor protocol (__get__, __set__, __delete__) for controlled attribute access:
class TypedAttribute:
def __init__(self, name, expected_type):
self.name = name
self.expected_type = expected_type
def __set__(self, instance, value):
print(f'Setting {self.name}')
if not isinstance(value, self.expected_type):
raise TypeError(f'{self.name} must be {self.expected_type.__name__}')
instance.__dict__[self.name] = value
def __get__(self, instance, owner):
print(f'Getting {self.name}')
return instance.__dict__[self.name]
def __delete__(self, instance):
print(f'Deleting {self.name}')
del instance.__dict__[self.name]
class Person:
name = TypedAttribute('name', str)
age = TypedAttribute('age', int)
person = Person()
person.name = 'Alice' # Invokes __set__
print(person.name) # Invokes __get__
person.age = 30 # Valid
# person.age = 'thirty' # TypeError: age must be int
Memory Management in Circular Data Structures
Problem Scenario
Python's garbage collector uses reference counting. In circular structures (trees, graphs), parent-child references create cycles that prevent immediate cleanup when references are deleted.
Solution
Use weak references that don't increment reference counts:
import sys
import weakref
class Node:
def __init__(self, value):
self.value = value
self.data = Data(value, self)
def __del__(self):
print('Node garbage collected')
class Data:
def __init__(self, value, node):
self.value = value
self.node = weakref.ref(node) # Weak reference
def __del__(self):
print('Data garbage collected')
def get_node(self):
return self.node()
node = Node(100)
del node
print('After deletion')
# Output:
# Node garbage collected
# Data garbage collected
# After deletion
Reference Counting Demonstration
import sys
class Resource:
def __del__(self):
print('Resource cleaned up')
obj = Resource()
print(f'Refcount: {sys.getrefcount(obj)}') # 2
ref = obj
print(f'Refcount: {sys.getrefcount(obj)}') # 3
del ref
print(f'Refcount: {sys.getrefcount(obj)}') # 2
obj = None
# Output: Resource cleaned up
Dynamic Method Invocation by Name
Problem Scenario
Working with multiple library classes that have different method names for the same functionality:
# Library 1: Circle has area()
# Library 2: Triangle has get_area()
# Library 3: Rectangle has getArea()
Solution
Use getattr or operator.methodcaller for dynamic method resolution:
from operator import methodcaller
# Simulated library classes
class Circle:
def __init__(self, r):
self.r = r
def area(self):
return self.r ** 2 * 3.14159
class Triangle:
def __init__(self, a, b, c):
self.a, self.b, self.c = a, b, c
def get_area(self):
s = (self.a + self.b + self.c) / 2
return (s * (s - self.a) * (s - self.b) * (s - self.c)) ** 0.5
class Rectangle:
def __init__(self, w, h):
self.w, self.h = w, h
def getArea(self):
return self.w * self.h
def calculate_area(shape, method_names=['area', 'get_area', 'getArea']):
for method_name in method_names:
if hasattr(shape, method_name):
return methodcaller(method_name)(shape)
# Alternative: return getattr(shape, method_name)()
raise AttributeError('No compatible area method found')
shapes = [Circle(1), Triangle(3, 4, 5), Rectangle(4, 6)]
areas = [calculate_area(s) for s in shapes]
print(areas) # [3.14159, 6.0, 24]