Fading Coder

One Final Commit for the Last Sprint

Home > Notes > Content

Python Advanced Class Design: Deep Object Manipulation Techniques

Notes May 10 3

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]

Related Articles

Designing Alertmanager Templates for Prometheus Notifications

How to craft Alertmanager templates to format alert messages, improving clarity and presentation. Alertmanager uses Go’s text/template engine with additional helper functions. Alerting rules referenc...

Deploying a Maven Web Application to Tomcat 9 Using the Tomcat Manager

Tomcat 9 does not provide a dedicated Maven plugin. The Tomcat Manager interface, however, is backward-compatible, so the Tomcat 7 Maven Plugin can be used to deploy to Tomcat 9. This guide shows two...

Skipping Errors in MySQL Asynchronous Replication

When a replica halts because the SQL thread encounters an error, you can resume replication by skipping the problematic event(s). Two common approaches are available. Methods to Skip Errors 1) Skip a...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.