Special Methods — Python Like You Mean It
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() ||