Understanding Python's Magic Methods: __new__, __init__, and __call__
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.