When we were introducing and exploring the dir
built-in function, we noticed that a lot of objects have attributes with "special" names, in particular names that start and end with a double-underscore (e.g., __init__
).
These attributes are special! And typically Python reserves special names for these attributes to give the objects special functionality.
We'll touch on a few common special methods below, but you can take a look at a more complete list of all of the special methods in the Python Data Model in the Python Documentation.
The common way to initialize instance data at construction time is with the __init__
method. For example, if you have a class with instance data y
, you can initialize the value of y
at construction time like so:
class MyClass1:
def __init__(self, y):
self.y = y
And you can initialize the value of y
when you create each instance:
mc1 = MyClass1(2)
mc1.y
When an object is "printed" or converted to a string, the __str__
special method is called. There is a default behavior for this for any object, but you can customize it with this method.
str(mc1)
class MyClass2:
def __str__(self):
return '<MyClass>'
mc2 = MyClass2()
str(mc2)
You can define "less than", "greator than", "equal to", "not equal to", "greator than or equal to", and "less than or equal to" meaning to your custom objects with the __lt__
, __gt__
, __eq__
, __ne__
, __ge__
, and __le__
methods.
class MyClass3:
def __init__(self, x=2):
self.x = x
def __lt__(self, other):
return self.x < other.x
def __gt__(self, other):
return self.x > other.x
mc3a = MyClass3(4)
mc3b = MyClass3(6)
mc3a < mc3b
mc3b > mc3a
Sometimes you want to create an object that "contains" a variable number of other things. Lists are like this, as are dictionaries. You can implement the __len__
object to allow the len()
built-in function to return the "length" (number of items contained) of the object.
class MyClass4:
def __init__(self, *args):
self.data = args
def __len__(self):
return len(self.data)
mc4 = MyClass4(1,2,3,4)
mc4.data
len(mc4)
When you create objects that "contain" other things, you sometimes want an easy way of seeing if something is "contained" by the object. This is handled with the in
keyword and you can implement how it works with the __contains__
special method.
class MyClass5:
def __init__(self, *args):
self.data = args
def __contains__(self, item):
return item in self.data
mc5 = MyClass5(1,2,3)
2 in mc5
4 in mc5
Dictionary-like key-value access to an object (i.e., object[key] = value
) can be provided with the __getitem__
and __setitem__
special methods.
class MyClass6:
def __init__(self, **kwargs):
self.data = kwargs
def __getitem__(self, key):
return self.data[key]
def __setitem__(self, key, value):
self.data[key] = value
mc6 = MyClass6(a=2, b=3, c=6)
mc6['a']
mc6['d'] = 8
mc6.data
There are actually a lot of special methods that you can implement to create numeric data-like operations, such as addition (__add__
, __radd__
, and __iadd__
), subtraction (__sub__
, __rsub__
, and __isub__
), multiplication (__mul__
, __rmul__
, and __imul__
), division (__truediv__
, __floordiv__
, ...), and others.
I won't go into all of this here, but I recommend you look at the Python documentation if you want more details.
class MyClass7:
def __init__(self, x):
self.x = x
def __add__(self, other):
if isinstance(other, MyClass7):
new_x = self.x + other.x
else:
new_x = self.x + other
return MyClass7(new_x)
def __sub__(self, other):
if isinstance(other, MyClass7):
new_x = self.x - other.x
else:
new_x = self.x - other
return MyClass7(new_x)
Numeric special methods can get complicated because you don't really know if other is an instance of a MyClass7 object, or not! You have to handle special cases manually.
mc7a = MyClass7(3)
mc7b = MyClass7(7)
mc7c = mc7a + mc7b
mc7c.x
mc7d = mc7a - mc7b
mc7d.x
mc7e = mc7a + 2
mc7e.x
If you want to deal with cases like 2 + mc7a, then you have to implement the __radd__ special method!
*There's so much you can do with Python special methods, and this just scratches the surface...*