Dunder objects (any object with name __***__
)#
Dunder (Double UNDERscore) objects have special meaning in Python. They define Python’s automated behavior such as item access and class initialization.
Important
You must pay attention when using dunder names, since they define Python’s language spec. Invalid modification on them can break your whole software.
On the other hand, you should modify them in sake of convenient and simple application development.
Methods to be defined by user#
__init__
#
Should return nothing (None
). This method is called every time you call a class
object, even if the call returns exact same instance as before (singleton pattern).
This is a good place to define instance variables the class holds, since this method is called every time a instance was created. In singleton class, you should pay attention about this feature, as it can cause unexpected initialization of the class state.
Example
A
is a class that accepts no argument on initializationclass A: def __init__(self): print("__init__ has been run")
For simple class defined above,
__init__
is run every time you callA
.>>> A() __init__ has been run >>> A() __init__ has been run
A
is a class that accepts arguments on initializationclass A: def __init__(self, a, b): print(f"__init__ has been run with arguments {a=}, {b=}")
In this case arguments to the class
A
immediately passed to__init__
.>>> A(1, "q") __init__ has been run with arguments a=1, b=q >>> A("abc", 123) __init__ has been run with arguments a=abc, b=123
A
is a singleton classclass A: _instance = None def __new__(cls, a, b): if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance def __init__(self, a, b): print(f"__init__ has been run with arguments {a=}, {b=}")
On the first call of
A
, the class creates new instanceinstance1
. After that, call onA
returns the exact same instance as the first one, soinstance2
is not a new instance but just another name forinstance1
.Even in this situation, the
__init__
method is called every time you call the classA
, no matter if new instance was created or not.>>> instance1 = A(1, "q") __init__ has been run with arguments a=1, b=q >>> instance2 = A("abc", 123) __init__ has been run with arguments a=abc, b=123 >>> instance1 is instance2 True
Tip
Setting a value to class variable from inside its instance is a little bit tricky. The
class variables can be accessed as self
’s attribute (self.<variable_name>
), but
setting a value in the same manner (self.<variable_name> = new_value
) won’t modify
the class variable, instead you may define new instance variable.
Since instance variables always take precedence on self
’s attribute search, the above
situation effectively obscure the class variable.
class A:
classvar = 1
def __init__(self, new_value):
print(f"{self.classvar=}")
self.classvar = new_value
print(f"{self.classvar=}")
>>> a = A(100)
self.classvar=1
self.classvar=100
>>> a.classvar
100 # Seems the class variable has been modified
>>> A.classvar
1 # But actually not
>>> a.__class__.__dict__
{'classvar': 1} # This is the class variable
>>> a.__dict__
{'classvar': 100} # This is the instance variable
__init_subclass__
#
Automatically classmethod
without decorator, should return nothing (None
). This
method is called every time you create subclass of the class which this method is
defined on.
__new__
#
Automatically classmethod
without decorator, should return class instance. Returning
something which is not an instance of the class or its subclass won’t raise any error,
but the behavior differs. See below for the details.
Example
A.__new__
returns an instance ofA
class A: def __new__(cls, *args, **kwargs): return super().__new__(cls) def __init__(self, *args, **kwargs): print("a")
In this case, the following 2 snippets are almost equivalent:
>>> A(1, 2, 3, a=4, b=5) a
>>> instance_of_A = A.__new__(A, 1, 2, 3, a=4, b=5) >>> instance_of_A.__init__(1, 2, 3, a=4, b=5) a
A.__new__
returns an instance ofB
, which is subclass ofA
class A: def __new__(cls, a, b): return super().__new__(B) def __init__(self, a, b): print(self.__class__.__name__) class B(A): ...
In this case, the behavior is almost the same as case 1, just minor swap of classes involved.
>>> A(1, 2, 3, a=4, b=5) B
>>> instance_of_B = A.__new__(A, 1, 2, 3, a=4, b=5) >>> instance_of_B.__init__(1, 2, 3, a=4, b=5) B
When class
B
is called, the behavior is exactly equivalent to case 1.>>> B(1, 2, 3, a=4, b=5) B
>>> instance_of_B = B.__new__(B, 1, 2, 3, a=4, b=5) >>> instance_of_B.__init__(1, 2, 3, a=4, b=5) B
A.__new__
returns an instance ofC
, which is irrelevant toA
class A: def __new__(cls, a, b): return super().__new__(C) def __init__(self, a, b): print(self.__class__.__name__) class C: ...