Special Methods
In this section, we will learn about a variety of instance methods that are reserved by Python, which affect an object’s high level behavior and its interactions with operators. These are known as special methods. __init__
is an example of a special method; recall that it controls the process of creating instances of a class. Similarly, we will see that __add__
controls the behavior of an object when it is operated on by the +
symbol, for example. In general, the names of special
methods take the form of __<name>__
, where the two underscores preceed and succeed the name. Accordingly, special methods can also be referred to as “dunder” (double-underscore) methods. Learning to leverage special methods will enable us to design elegant and powerful classes of objects.
These methods give us complete control over the various high-level interfaces that we use to interact with objects. Let’s make a simple class with nonsensical behavior to demonstrate our ability to shape how our class behaves:
# Demonstrating (mis)use of special methods
class SillyClass:
def __getitem__(self, key):
""" Determines behavior of `self[key]` """
return [True, False, True, False]
def __pow__(self, other):
""" Determines behavior of `self ** other` """
return "Python Like You Mean It"
>>> silly = SillyClass()
>>> silly[None]
[True, False, True, False]
>>> silly ** 2
'Python Like You Mean It'
This section is not meant to be a comprehensive treatment of special methods, which would require us to reach beyond our desired level of sophistication. The official Python documentation provides a rigorous but somewhat inaccessible treatment of special methods. Dive into Python 3 has an excellent appendix on special methods. It is strongly recommended that readers consult this resource.
String-Representations of Objects
The following methods determines how an object should be represented as a string in various contexts. For example, this text consistently utilizes the fact that passing an object to the Python console will prompt the console to print out a representation of that object as a string. That is,
>>> x = list(("a", 1, True))
>>> x
['a', 1, True]
Under the hood, the special method x.__repr__
is being called to obtain this string representation whenever an object is displayed in a console/notebook like this. The method returns the string "['a', 1, True]"
, which is then printed out to the console. This is an extremely useful for creating classes whose objects can be inspected conveniently in a Python console or in a Jupyter notebook. Similarly __str__
returns the string that will be produced when str
is called on the
object.
Method |
Signature |
Explanation |
---|---|---|
Returns string for a printable representation of object |
|
|
Returns string representation of an object |
|
|
A well-implemented __repr__
method can greatly improve the convenience of working with a class. For example, let’s add this method to our ShoppingList
class that we wrote in the preceding section; the __repr__
will create a string with our shopping items on a bulleted list with purchased items crossed out:
def strike(text):
""" Renders string with strike-through characters through it.
`strike('hello world')` -> '̶h̶e̶l̶l̶o̶ ̶w̶o̶r̶l̶d'
Notes
-----
\u0336 is a special strike-through unicode character; it
is not unique to Python."""
return ''.join('\u0336{}'.format(c) for c in text)
class ShoppingList:
def __init__(self, items):
self._needed = set(items)
self._purchased = set()
def __repr__(self):
""" Returns formatted shopping list as a string with
purchased items being crossed out.
Returns
-------
str"""
if self._needed or self._purchased:
remaining_items = [str(i) for i in self._needed]
purchased_items = [strike(str(i)) for i in self._purchased]
# You wont find the • character on your keyboard. I simply
# googled "unicode bullet point" and copied/pasted it here.
return "• " + "\n• ".join(remaining_items + purchased_items)
def add_new_items(self, items):
self._needed.update(items)
def mark_purchased_items(self, items):
self._purchased.update(set(items) & self._needed)
self._needed.difference_update(self._purchased)
# demonstrating `ShoppingList.__repr__`
>>> l = ShoppingList(["grapes", "beets", "apples", "milk", "melon", "coffee"])
>>> l.mark_purchased_items(["grapes", "beets", "milk"])
>>> l
• melon
• apples
• coffee
• ̶g̶r̶a̶p̶e̶s
• ̶m̶i̶l̶k
• ̶b̶e̶e̶t̶s
See that this simple method makes it much easier for us to inspect the state of our shopping list when we are working in a console/notebook environment.
Interfacing with Mathematical Operators
The following special methods control how an object interacts with +
, *
, **
, and other mathematical operators. A full listing of all the special methods used to emulate numeric types can be found here
Method |
Signature |
Explanation |
---|---|---|
Add |
|
|
Subtract |
|
|
Multiply |
|
|
Divide |
|
|
Power |
|
|
You may be wondering why division has the peculiar name __truediv__
, whereas the other operators have more sensible names. This is an artifact of the transition from Python 2 to Python 3; the default integer-division was replaced by float-division, and thus __div__
was replaced by __truediv__
for the sake of 2-3 compatibility.
Let’s give ShoppingList
an __add__
method so that we can merge shopping lists using the +
operator. Rather than redefine the entire ShoppingList
class, we can simply define this as a function and use setattr
to set it as a method to our existing class.
def __add__(self, other):
""" Add the unpurchased and purchased items from another shopping
list to the present one.
Parameters
----------
other : ShoppingList
The shopping list whose items we will add to the present one.
Returns
-------
ShoppingList
The present shopping list, with items added to it."""
new_list = ShoppingList([])
# populate new_list with items from `self` and `other`
for l in [self, other]:
new_list.add_new_items(l._needed)
# add purchased items to list, then mark as purchased
new_list.add_new_items(l._purchased)
new_list.mark_purchased_items(l._purchased)
return new_list
# set `__add__` as a method of `ShoppingList`
>>> setattr(ShoppingList, "__add__", __add__)
Now let’s create a few shopping lists and combine them:
>>> food = ShoppingList(["milk", "flour", "salt", "eggs"])
>>> food.mark_purchased_items(["flour", "salt"])
>>> office_supplies = ShoppingList(["staples", "pens", "pencils"])
>>> office_supplies.mark_purchased_items(["pencils"])
>>> clothes = ShoppingList(["t-shirts", "socks"])
# combine all three shopping lists
>>> food + office_supplies + clothes
• t-shirts
• eggs
• pens
• milk
• staples
• socks
• ̶f̶l̶o̶u̶r
• ̶s̶a̶l̶t
• ̶p̶e̶n̶c̶i̶l̶s
Overloading the +
operator provides us with a sleek interface for merging multiple shopping lists in a sleek, readable way. food + office_supplies + clothes
is equivalent to calling (food.__add__(office_supplies)).__add__(clothes)
. It is obvious that the former expression is far superior.
Creating a Container-Like Class
The following special methods allow us to give our class a container interface, like that of a dictionary, set, or list. An exhaustive listing and discussion of these methods can be found here
Method |
Signature |
Explanation |
---|---|---|
Length |
|
|
Get Item |
|
|
Set Item |
|
|
Contains |
|
|
Iterator |
|
|
Next |
|
|
To get a feel for these methods, let’s create class that implements most aspects of a list’s interface. We will store a list as an attribute of our class to keep track of the contents, but will implement special methods that “echo” the interface of the list.
class MyList:
def __init__(self, *args):
if len(args) == 1 and hasattr(args[0], '__iter__'):
# handles `MyList([1, 2, 3])
self._data = list(args[0])
else:
# handles `MyList(1, 2, 3)`
self._data = list(args)
def __getitem__(self, index):
out = self._data[index]
# slicing should return a `MyList` instance
# otherwise, the individual element should be returned as-is
return MyList(out) if isinstance(index, slice) else out
def __setitem__(self, key, value):
self._data[key] = value
def __len__(self):
return len(self._data)
def __repr__(self):
""" Use the character | as the delimiter for our list"""
# `self._data.__repr__()` returns '[ ... ]',
# thus we can slice to get the contents of the string
# and exclude the square-brackets, and add our own
# delimiters in their place
return "|" + self._data.__repr__()[1:-1] + "|"
def __contains__(self, item):
return item in self._data
def append(self, item):
self._data.append(item)
Let’s appreciate the rich behavior that we get out of this simple class:
# MyList can accept any iterable as its
# first (and only) input argument
>>> x = MyList("hello")
>>> x
|'h', 'e', 'l', 'l', 'o'|
# MyList accepts an arbitrary number of arguments
>>> x = MyList(1, 2, 3, 4, 5)
>>> x
|1, 2, 3, 4, 5|
>>> len(x)
5
# getting an item
>>> x[0]
1
# slicing returns a MyList instance
>>> x[2:4]
|3, 4|
# setting an item
>>> x[0] = -1
>>> x
|-1, 2, 3, 4, 5|
# checking membership
>>> 10 in x
False
>>> MyList()
||