Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Understanding Python's Magic Methods: __new__, __init__, and __call__

Tech May 12 2

The Initialization Method: __init__

When defining classes in Python, __init__ is the most commonly overridden method. It serves as the initializer, responsible for setting up the instance's atributes immediately after the object is created. The first parameter, self, refers to the instance being initialized, followed by any arguments required to configure the object.

class UserProfile:
    def __init__(self, username, identifier):
        self.username = username
        self.identifier = identifier

if __name__ == '__main__':
    user = UserProfile('jdoe', 101)
    print(user.username)  # Output: jdoe
    print(user.identifier)  # Output: 101

The Construction Method: __new__

While __init__ handles initialization, __new__ is responsible for the actual construction of the instance. It is a static method that controls the creation of a new object. Python calls __new__ before __init__. The primary role of __new__ is to allocate memory and return the instance reference, which is then passed as self to __init__.

If a class does not define __new__, Python inherits it from the base object class. The default object.__new__ logic handles basic memory allocation.

class Box:
    def __new__(cls, *args, **kwargs):
        print(f"Allocating memory for class: {cls}")
        print(f"Arguments received: {args}, {kwargs}")
        instance = super().__new__(cls)
        print(f"Instance address: {hex(id(instance))}")
        return instance

    def __init__(self, capacity):
        print(f"Initializing Box with capacity: {capacity}")
        self.capacity = capacity

# Instantiation
b = Box(50, color='red')

Output:

Allocating memory for class: <class '__main__.Box'>
Arguments received: (50,), {'color': 'red'}
Instance address: 0x...
Initializing Box with capacity: 50

Returning an Instance is Mandatory

The __new__ method must return an instance of the class (or another compatible class). If it returns None, the __init__ method will not be called because there is no valid object to initialize.

class FaultyClass:
    def __new__(cls):
        print("__new__ executed")
        return None  # Fails to return an instance

    def __init__(self):
        print("__init__ executed")

obj = FaultyClass()
print(obj)  # Output: None

Returning a Different Class Instance

__new__ is not strictly required to return an instance of the class it belongs to (cls). It can return an instance of a different class. If this happens, only the __init__ of the returned object's class is called.

class Target:
    pass

class Source:
    def __new__(cls, *args, **kwargs):
        return Target.__new__(Target)

obj = Source()
print(type(obj))  # Output: <class '__main__.Target'>

Parameter Handling in __new__

The signature for __new__ is typically __new__(cls, *args, **kwargs). While *args and **kwargs receive the instantiation arguments, they are not strictly required by the default object.__new__ implementation, which only accepts the class argument. However, accepting these parameters allows for custom logic, such as immutable type customization or validation before instance creation.

Use Case: Customizing Immutable Types

Since immutable types (like int or str) cannot be modified after creation, __new__ is the place to enforce constraints.

class PositiveInteger(int):
    def __new__(cls, value):
        if value <= 0:
            raise ValueError("Value must be positive")
        return super().__new__(cls, value)

try:
    num = PositiveInteger(10)
    print(num)  # 10
    PositiveInteger(-5)
except ValueError as e:
    print(e)  # Value must be positive

The Invocation Method: __call__

In Python, objects can behave like functions if the class defines the __call__ method. When an instance is called using parentheses (e.g., instance()), the __call__ method is executed. This effectively bridges the gap between object-oriented programming and functional programming styles.

class Greeter:
    def __call__(self, name):
        return f"Hello, {name}!"

g = Greeter()
print(g("Alice"))  # Output: Hello, Alice!

Checking Callability

The built-in callable() function determines if an object can be invoked.

print(callable(5))         # False
print(callable(Greeter))   # True (Classes are callable)
print(callable(g))         # True (Instance has __call__)

Practical Applications of __call__

1. Implementing Class-Based Decorators

Decorators implemented as classes must use __call__ to wrap the target function.

class Tracer:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print(f"Calling {self.func.__name__}")
        return self.func(*args, **kwargs)

@Tracer
def process_data(x):
    return x * 2

print(process_data(5))
# Output:
# Calling process_data
# 10

2. Simplifying Complex Workflows

If an object represents a process with a standard sequence of steps, __call__ can encapsulate the entire workflow into a single invocation.

class ImageProcessor:
    def load(self):
        print("Loading image...")

    def resize(self):
        print("Resizing image...")

    def filter(self):
        print("Applying filters...")

    def save(self):
        print("Saving image...")

    def __call__(self):
        self.load()
        self.resize()
        self.filter()
        self.save()

processor = ImageProcessor()
processor()  # Executes the full pipeline

3. Improving API Consistency

__call__ allows instances to be used interchangeably with functions, treating them as "functors." This is useful for strategies or command patterns where the distinction between a functon and a stateful object should be abstracted away.

class Command:
    def __init__(self, action):
        self.action = action

    def __call__(self):
        print(f"Executing: {self.action}")

def simple_task():
    print("Executing: simple_task")

def execute(task):
    task()

execute(Command("Deploy"))
execute(simple_task)

Summary of Relationships

  • __new__: The constructor. A static method responsible for creating the instance and allocating memory. It runs first.
  • __init__: The initializer. An instance method responsible for configuring the created instance. It runs second.
  • __call__: The invoker. An instance method that enables the object to be called like a function.
Tags: PythonOOP

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

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