Understanding the Differences Between Self and __init__ Methods in Python Classes

πŸ’‘ Problem Formulation: When delving into Python classes, newcomers may confuse the use of self and __init__. The issue arises with understanding why both exist and how they differ in terms of functionality. In this article, we will demystify these Python class components with distinct examples. We’ll show how the self argument represents an instance of the class, while __init__ is a method that initializes the instance upon creation.

Method 1: Functionality of self

Within a Python class, self represents the instance of the class, allowing access to the instance’s attributes and methods. When you define a method within a class, self is always the first parameter. It must be explicitly listed in the definition, but you do not pass it when calling the method. Consider it as a reference to the ‘current object’.

Here’s an example:

class MyClass:
    def __init__(self, value):
        self.attribute = value
    
    def show(self):
        print(self.attribute)

obj = MyClass("Hello, self!")
obj.show()

Output:

Hello, self!

In the above example, self within the show method refers to obj, which is an instance of MyClass. It is used to access the class attribute attribute that was initialized in the __init__ method.

Method 2: The role of __init__ method

The __init__ method in Python is a special method also known as the constructor. It is called automatically when a new instance of a class is created. The primary role of __init__ is to initialize the state of an instance by assigning values to its properties or performing any setup procedures.

Here’s an example:

class MyClass:
    def __init__(self, value):
        self.attribute = value

obj = MyClass("Initialized with __init__")
print(obj.attribute)

Output:

Initialized with __init__

In this snippet, the __init__ method initializes the attribute of the class instance obj with the string passed as an argument. This is the first method called after an instance is created, setting the initial state of the object.

Method 3: When to use self vs __init__

Use self to refer to and modify the state of a class instance within any instance method. Use __init__ to set up the initial state of an instance when the class is first instantiated. In other words, __init__ is only called once per instance, while self is used throughout the class to refer to the specific instance within the class’s methods.

Here’s an example:

class MyClass:
    def __init__(self, value):
        self.attribute = value
    
    def update(self, new_value):
        self.attribute = new_value

obj = MyClass("Initial Value")
print(obj.attribute)
obj.update("Updated Value")
print(obj.attribute)

Output:

Initial Value
Updated Value

The update method uses self to modify the object’s attribute, showcasing self in action outside of the __init__ method. We see that the class’s state can be altered beyond initialization with self.

Method 4: Shared attributes and self

Class attributes, shared by all instances, are not typically associated with self but can be accessed through it. Since self is a reference to the instance, it provides a pathway to even class attributes, which aren’t bound to any one instance. On the other hand, instance attributes initialized in the __init__ method are unique to each instance and are directly associated with self.

Here’s an example:

class MyClass:
    shared_attribute = "I am shared"
    def __init__(self):
        self.unique_attribute = "I am unique"

obj1 = MyClass()
obj2 = MyClass()
print(obj1.shared_attribute)
obj2.shared_attribute = "Changed shared"
print(obj1.shared_attribute)

Output:

I am shared
I am shared

This code illustrates that altering a perceived class attribute via self does not affect the class attribute itself, but rather shadows it by creating a new instance attribute of the same name.

Bonus One-Liner Method 5: Chaining Methods with self

Using self, you can chain methods within the class, enabling a fluent interface that allows multiple method calls in a single statement. This happens as each method returns the self instance, facilitating the subsequent method calls.

Here’s an example:

class MyClass:
    def __init__(self):
        self.value = 0
    def increment(self):
        self.value += 1
        return self
    def display(self):
        print(self.value)

obj = MyClass().increment().increment().display()

Output:

2

The chaining of increment and display methods demonstrates how returning self can be used for method chaining, providing a concise and fluent way to manipulate object state and behavior.

Summary/Discussion

  • Method 1: self as instance reference. Self represents the instance allowing attribute and method access. It cannot initialize attributes.
  • Method 2: __init__ as initializer. The constructor method for setting up instances with their initial state. It is called only once per instance creation.
  • Method 3: Usage context. Self is used throughout class methods while __init__ is specifically for initialization.
  • Method 4: Shared vs. unique attributes. Self can access both shared (class) attributes and unique (instance) attributes. Changes via self usually affect only the instance.
  • Bonus Method 5: Method chaining. Self allows for method chaining by returning the instance itself from methods, thus enabling fluent interfaces.