Python Object Composition, Reflection, and Magic Methods Explained

Object Composition, Reflection, and Magic Methods

Object Composition

Composition is a fundamental concept in object-oriented programming where one object contains a reference to another object as one of its attributes. This approach enables building complex objects by combining simpler ones.

When to use inheritance versus composition:

  • Inheritance (is-a relationship): Use when one class is a specialized version of another. For example, a Dog is a Animal.
  • Composition (has-a relationship): Use when one class has another as a component. For example, a Car has an Engine.

Basic Composition Example

class Engine:
    def __init__(self, horsepower):
        self.horsepower = horsepower

class Car:
    def __init__(self, model, engine):
        self.model = model
        self.engine = engine  # Composition: Car has an Engine

vehicle = Car("Tesla", Engine(400))
print(vehicle.engine.horsepower)  # Output: 400

Course Seletcion System

This example demonstrates practical composition usage in a course enrollment system:

class Person:
    institution = "Tech University"

    def __init__(self, name, age, gender):
        self.name = name
        self.age = age
        self.gender = gender


class Administrator(Person):
    def __init__(self, name, age, gender):
        super().__init__(name, age, gender)


class Course:
    def __init__(self, title, fee, duration):
        self.title = title
        self.fee = fee
        self.duration = duration


python_course = Course("Python Programming", 15000, "6 months")
linux_course = Course("Linux Administration", 12000, "5 months")


class Student(Person):
    def __init__(self, name, age, gender, enrolled_courses=None):
        super().__init__(name, age, gender)
        self.enrolled_courses = enrolled_courses or []


student = Student("Alice", 22, "female")
student.enrolled_courses.append(python_course)
student.enrolled_courses.append(linux_course)

for course in student.enrolled_courses:
    print(f"{course.title}: ${course.fee}")


class Instructor(Person):
    def __init__(self, name, age, gender, certification_level):
        super().__init__(name, age, gender)
        self.certification_level = certification_level
        self.assigned_course = None


instructor = Instructor("Dr. Smith", 35, "male", 10)
instructor.assigned_course = linux_course

print(f"Instructor: {instructor.name}")
print(f"Course: {instructor.assigned_course.title}")
print(f"Duration: {instructor.assigned_course.duration}")

Reflection in Python

Reflection enables runtime inspection and modification of objects and classes. Python provides four built-in functions for reflection operations. These functions work on both classes and instances since everything in Python is an object.

Retrieving Attributes with getattr()

The getattr() function retrieves attributes by name. If the attribute doesn't exist, it returns a default value if provided.

class Student:
    university = "MIT"

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def display_info(self):
        print(f"Student: {self.name}, Age: {self.age}")

    def calculate_grade(self):
        print("Calculating grade...")


student = Student("Bob", 21)

# Direct attribute access
print(student.name)  # Bob

# Reflected attribute access
campus = getattr(student, "university", "Unknown")
print(campus)  # MIT

# Non-existent attribute with default
result = getattr(student, "gpa", 0.0)
print(result)  # 0.0

# Accessing methods dynamically
method = getattr(student, "display_info", None)
if method:
    method()

Setting Attributes with setattr()

The setattr() function dynamically adds or modifies attributes on objects.

class Student:
    university = "MIT"

    def __init__(self, name, age):
        self.name = name
        self.age = age


student = Student("Charlie", 23)
setattr(student, "gpa", 3.8)
setattr(student, "major", "Computer Science")

print(student.__dict__)
# {'name': 'Charlie', 'age': 23, 'gpa': 3.8, 'major': 'Computer Science'}

Checking Attributes with hasattr()

The hasattr() function verifies whether an object possesses a specific attribute.

class Student:
    university = "MIT"

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def display_info(self):
        print(f"Student: {self.name}")


student = Student("Diana", 20)

if hasattr(student, "display_info"):
    getattr(student, "display_info")()

if hasattr(student, "gpa"):
    print(f"GPA: {student.gpa}")
else:
    print("No GPA attribute")

Deleting Attributes with delattr()

The delattr() function removes attributes from objects.

class Student:
    university = "MIT"

    def __init__(self, name, age, gpa):
        self.name = name
        self.age = age
        self.gpa = gpa


student = Student("Eve", 22, 3.5)
print(student.__dict__)  # All attributes present

delattr(student, "gpa")
print(student.__dict__)  # gpa removed

Magic Methods

Magic methods (also known as dunder methods) are special methods that enable custom classes to integrate with Python's built-in operations and syntax.

String Representation: __str__ and __repr__

These methods define how objects are displayed as strings.

class Student:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        """Called by str() and print() functions"""
        print("__str__ invoked")
        return f"Student: {self.name} (Age: {self.age})"

    def __repr__(self):
        """Called by repr() function, useful for debugging"""
        print("__repr__ invoked")
        return f"Student('{self.name}', {self.age})"


student = Student("Frank", 25)

print(student)       # Uses __str__
print(str(student))  # Uses __str__

# Explicit repr call
print(repr(student))  # Uses __repr__

Cleanup: __del__ Method

The __del__ method acts as a destructor, called when an object is garbage collected or when the program exits.

class DatabaseConnection:
    def __init__(self, host, port):
        self.host = host
        self.port = port
        self.connection = open("connection_log.txt", "a")

    def query(self, sql):
        print(f"Executing: {sql}")

    def __del__(self):
        """Ensure resources are released when object is destroyed"""
        print("Cleaning up database connection")
        self.connection.close()


db = DatabaseConnection("localhost", 5432)
db.query("SELECT * FROM users")

Checking Class Hierarchy: issubclass()

The issubclass() functon determines if one class inherits from another.

class Animal:
    pass

class Mammal(Animal):
    pass

class Bird:
    pass

print(issubclass(Mammal, Animal))  # True
print(issubclass(Bird, Animal))    # False
print(issubclass(Mammal, object))  # True

Documentation: __doc__ Attribute

Class docstrings are stored in the __doc__ attribute. Note that docstrings are not inherited by subclasses.

class Animal:
    """
    Base class for all animals.
    Provides common attributes like name and age.
    """
    pass


class Dog(Animal):
    """Represents a canine companion."""
    pass


print(Animal.__doc__)
# Base class for all animals.
# Provides common attributes like name and age.

print(Dog.__doc__)
# Represents a canine companion.

Context Managers: __enter__ and __exit__

These methods enable the with statement, providing resource management and exception handling.

class FileHandler:
    def __init__(self, filename):
        self.filename = filename

    def __enter__(self):
        """Called when entering the with block"""
        print("Setting up file resources")
        return self

    def __exit__(self, exception_type, exception_value, traceback):
        """Called when exiting the with block"""
        print("Cleaning up file resources")
        if exception_type:
            print(f"Exception handled: {exception_type.__name__}")
        return True  # Suppress exceptions


# Successful execution
with FileHandler("data.txt") as handler:
    print("Performing file operations")
    print("File operations completed")

# Exception handling demonstration
with FileHandler("data.txt") as handler:
    print("About to process data")
    raise ValueError("Invalid data format")
    print("This line never executes")

print("Program continues after exception")

Attribute Access: __getattr__, __setattr__, __delattr__

These methods intercept attribute operations using dot notation.

class DynamicObject:
    class_variable = 1

    def __init__(self, value):
        self.value = value

    def __getattr__(self, name):
        """Called when attribute doesn't exist"""
        print(f"Attempted access to non-existent attribute: {name}")
        return None

    def __setattr__(self, name, value):
        """Called when setting any attribute"""
        print(f"Setting attribute: {name} = {value}")
        self.__dict__[name] = value

    def __delattr__(self, name):
        """Called when deleting any attribute"""
        print(f"Deleting attribute: {name}")
        if name in self.__dict__:
            del self.__dict__[name]


obj = DynamicObject(100)
print(f"obj.value: {obj.value}")

obj.new_property = 42
print(f"obj.new_property: {obj.new_property}")

nonexistent = obj.missing_attribute
print(f"Result: {nonexistent}")

del obj.new_property

Item Access: __getitem__, __setitem__, __delitem__

These methods enable bracket notation for attribute access, similar to dictionary syntax.

class Container:
    def __init__(self, name):
        self.name = name

    def __getitem__(self, key):
        """Called when accessing obj[key]"""
        print(f"Getting item: {key}")
        return self.__dict__[key]

    def __setitem__(self, key, value):
        """Called when setting obj[key] = value"""
        print(f"Setting item: {key} = {value}")
        self.__dict__[key] = value

    def __delitem__(self, key):
        """Called when deleting obj[key]"""
        print(f"Deleting item: {key}")
        if key in self.__dict__:
            del self.__dict__[key]


container = Container("MyContainer")

container["data"] = [1, 2, 3]
print(container.data)

del container["data"]
print(container.__dict__)

Callable Objects: __call__

The __call__ method enables instances to be called as functions.

class Multiplier:
    def __init__(self, factor):
        self.factor = factor

    def __call__(self, value):
        """Makes the instance callable like a function"""
        return value * self.factor


double = Multiplier(2)
triple = Multiplier(3)

result1 = double(10)  # Calls double.__call__(10)
result2 = triple(10)  # Calls triple.__call__(10)

print(f"Doubled: {result1}")  # 20
print(f"Tripled: {result2}")  # 30

print(f"Is callable: {callable(double)}")  # True

Tags: python OOP composition reflection magic-methods

Posted on Fri, 16 Oct 2026 16:54:30 +0000 by Peuplarchie