Dunder Methods

Dunder Methods

Let’s talk about this __ thing we keep seeing around method names.

We call these methods “dunder” methods which stands for “double under” or “double underscore”.

Python uses an assortment of dunder methods to allow for operator overloading and implementing various other protocols. These methods start and end with two underscores.

In real life, we never need to call these methods ourselves. They are only used for customizing behavior of other functions and operators. We show calling them here so you can see what they do and why we implement them.

These are sometimes called “magic methods” but they are not magical. You can define dunder methods to customize the behavior of your own classes.

Review

We have already seen some dunder methods. Let’s review.

class BankAccount:

    def __init__(self, balance=0):
        self.balance = balance

    def __str__(self):
        return f"Account with balance of ${self.balance}"

    def __repr__(self):
        return f"BankAccount(balance={self.balance})"

    def deposit(self, amount):
        self.balance += amount

    def withdraw(self, amount):
        self.balance -= amount

Our BankAccount class had:

  • an __init__ method that is called to initialize the class

  • an __str__ method that is called when converting the object to a human-readable string

  • a __repr__ method that is called when converting the object to a developer-readable string

More Duck Typing

We briefly talked about “duck typing” earlier.

Duck typing means that we pretty much never care about the types of objects. Instead of checking what the type of an object is, we instead check whether it implements a feature.

Dunder methods provide a way for our class to customize operators and other built-in Python behavior for our objects.

Operator Overloading

>>> "Hello " + "World"
'Hello World'
>>> "Hello " * 3
'Hello Hello Hello '
>>> "Hello " - "H"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for -: 'str' and 'str'

When we refer to “operator overloading” we really just mean implementing a particular operator’s behavior on our object.

Strings are overloaded for + and * but not -.

Arithmetic Operators

>>> a = {1, 2}
>>> b = {2, 3}
>>> a.__sub__(b)
{1}
>>> a - b
{1}
>>> "hello ".__add__("world")
'hello world'
>>> "hello " + "world"
'hello world'
>>> x = 3
>>> y = 2
>>> x.__mul__(y)
6
>>> x * y
6

Comparison Operators

>>> x = 2
>>> y = 5
>>> x.__lt__(y)
True
>>> x < y
True
>>> x.__eq__(y)
False
>>> x == y
False
>>> x.__gt__(y)
False
>>> x > y
False
>>> x.__ne__(y)
True
>>> x != y
True
>>> x.__ge__(y)
False
>>> x >= y
False
>>> x.__le__(y)
True
>>> x <= y
True

Truthiness

How do we determine the truthiness of an object?

>>> x = 2
>>> y = 0
>>> bool(x)
True
>>> bool(y)
False

Truthiness is also something we can customize with dunder methods:

>>> x.__bool__()
True
>>> y.__bool__()
False

Note

bool checks for a non-zero return value from __len__ if no __bool__ method is available.

Callable

We can use the __call__ method to make a class instance callable:

class NameSayer:

    def __init__(self, name):
        self.name = name

    def __call__(self):
        print(f"Hi {self.name}")

We can call our class instance just like any other function:

>>> say_trey = NameSayer("Trey")
>>> say_trey
<NameSayer object at 0x...>
>>> type(say_trey)
<class 'NameSayer'>
>>> say_trey()
Hi Trey

Dunder Exercises

Pythonic Points

Edit the Point class in the properties.py file to:

  1. Support using the + operator between two points

  2. Support using the * operator between two a point and a number

  3. Support the == and != operators between two points

Note

To test these changes, you should modify the PointTests class in properties_test.py to comment out the @unittest.skip for “Pythonic Points”.

>>> p1 = Point(1, 2, 3)
>>> p2 = Point(4, 5, 6)
>>> p1 + p2
Point(5, 7, 9)
>>> p1 * 2
Point(2, 4, 6)
>>> p1 == p2
False
>>> p1 + Point(3, 3, 3) == p2
True

Truthy Account

Edit the BankAccount class in the classes.py file to make account objects truthy when the bank account has money in it.

Note

To test these changes, you should modify the BankAccountTests class in classes_test.py to comment out the lines with “Comment this line for truthy accounts exercise.”

Example usage:

>>> from classes import BankAccount
>>> trey_account = BankAccount(100)
>>> bool(trey_account)
True
>>> trey_account.withdraw(100)
>>> bool(trey_account)
False
>>> trey_account.withdraw(1)
>>> bool(trey_account)
False

You can use this class:

class BankAccount:

    """Bank account that supports truthiness."""

    def __init__(self, balance=0):
        self.balance = balance

    def __repr__(self):
        return f"{type(self).__name__}(balance={self.balance})"

    def deposit(self, amount):
        self.balance += amount

    def withdraw(self, amount):
        self.balance -= amount

    def transfer(self, other_account, amount):
        self.withdraw(amount)
        other_account.deposit(amount)

Hint

Use the __bool__ method

Comparable Account

Edit the BankAccount class in the classes.py file to allow accounts to be compared using arithmetic comparison operators.

Note

To test these changes, you should modify the BankAccountTests class in classes_test.py to comment out the line with “Comment this line for BankAccount comparison exercise.”

Example:

>>> from classes import BankAccount
>>> account1 = BankAccount(balance=100)
>>> account2 = BankAccount(balance=200)
>>> account1 == account2
False
>>> account1 < account2
True
>>> account1 >= account2
False

Caseless String

This is the CaselessString exercise in dunder.py.

Use UserString to make a CaselessString class which does comparisons in a case insensitive manner.

Example:

>>> from dunder import CaselessString
>>> s1 = CaselessString("Hello there")
>>> s2 = CaselessString("hello there")
>>> s3 = CaselessString("HELLO THERE")
>>> s1 == s2 == s3
True
>>> s1.lower() == s2.upper()
True

Easy Dict

Edit the EasyDict class in the dunder.py file to make an EasyDict class that can be used with both attribute and item syntax.

Run python test.py EasyDict in your exercises directory.

Example:

>>> from dunder import EasyDict
>>> a = EasyDict()
>>> a['shoe'] = "blue"
>>> a.shoe
"blue"
>>> a['shoe']
"blue"
>>> a.car = "green"
>>> a['car']
"green"

Hint

Use the __getitem__ and __setitem__ methods

Cyclic List

This is the CyclicList exercise in dunder.py.

Create a list-like data structure that loops in a cyclic manner:

>>> from dunder import CyclicList
>>> numbers = CyclicList([1, 2, 3, 4])
>>> numbers[1]
2
>>> numbers[2]
3
>>> numbers[12]
1
>>> [x for x, _ in zip(numbers, range(10))]
[1, 2, 3, 4, 1, 2, 3, 4, 1, 2]
>>> numbers[35]
4