Dunder Methods

Introduction to Software Engineering (CSSE 1001)

Author

Paul Vrbik

Published

March 13, 2025

Magic / Dunder Methods

Overloading built-in functions. Underscores

How underscores are used in python. (List not comprehensive.)

  1. As anonymous variables like in for _ in [1, 2, 3] or x, _, z = (1, 2, 3).
  2. For giving special meaning to function and names.
    1. _private variables.
    2. __names__ are for Python’s magic methods like __init__().

Initializer

Runs when the object is instantiated (i.e. created).

class Fraction():
    def __init__(self, numer: int, denom: int) -> None:
        self._numer = numer
        self._denom = denom

String Representation

The string representation says what to display when printing the object.

class Fraction():
    def __str__(self) -> str:
        return f"{self._numer} / {self._denom}"
p = Fraction(2, 3)
p
Fraction(2, 3)
print(p)
2 / 3

Representation

The representation of an object is what Python displays it on console and should be enough information to re-instantiate the object.

class Fraction():
    def __repr__(self) -> str:
        return f"Fraction({self._numer}, {self._denom})"
p = Fraction(2, 3) 
p
Fraction(2, 3)

Equality

We can specify that objects are equal for reasons other than sharing memory location using eq.

class Fraction():
    def __eq__(self, other) -> bool:  # note use of "other"
        a, b = self._numer, self._denom
        c, d = other._numer, other._denom
        return a*d == b*c
p = Fraction(4, 6)
q = Fraction(2, 3)
p == q
False

Add

Add instructs Python on how to add two objects together.

from __future__ import annotations  # for class type hint.
class Fraction():
    def __add__(self, other) -> Fraction: 
        a, b = self._numer, self._denom
        c, d = other._numer, other._denom
        return Fraction(a*d + c*b, b*d)
p = Fraction(2, 3)  
q = Fraction(1, 2)  
p + q
Fraction(7, 6)
Binary Operator Magic Method
+ __add__
- __sub__
* __mul__
** __pow__
// __floordiv__
/ __truediv__
Unary Operator Magic Method
- __neg__
abs __abs__
~ __invert__
Comparison Magic Method
< __lt__
<= __le__
== __eq__
!= __ne__
> __gt__
>= __ge__

Instance V. Class Variables

Recall the class we wrote for counting clicks:

class Clicker():
    def __init__(self) -> None:
        self._clicks = 0          # each instance has its own

    def click(self) -> None:
        self._clicks += 1

Can we calculate the number of clicks across all counters?

We can using a class variable.

class Clicker():
    _all_clicks = 0               # every instance has access to this

    def __init__(self) -> None:
        self._clicks = 0

    def click(self) -> None:
        self._clicks += 1         # access instance variable
        Clicker._all_clicks += 1  # access class variable
c = Clicker(); d = Clicker(); e = Clicker()  
# semi-colons can be used instead of newlines
c.click(); c.click(); c.click();
d.click(); d.click();
e.click()

We can see that these clicks have indeed been registered by the instance.

Although it is technically bad practice to access private variables, we are only doing so in an exploratory manner which is okay.

(c._clicks, d._clicks, e._clicks)
(3, 2, 1)
(c._all_clicks, d._all_clicks, e._all_clicks) 
(6, 6, 6)

We do not even require an instance!

Clicker._all_clicks
6

Exercises

Exercise 1 (Alarm) Create a class with the following functionality.

a = Alarm(5)
a.wait(1)
a
'Alarm pending.'
a.wait(2)
a
'Alarm pending.'
B.wait(3)
*Beep beep beep*

Exercise 2 (Vectors) Notice that + concatenates lists

[1, 2, 3] + [4, 5, 6]
[1, 2, 3, 4, 5, 6]

Implement a vector class so that we can do

x = Vector(1, 2)
y = Vector(3, 4)
x + y
<4, 6>
-x
<-1, -2>

Vector Starter Code

class Vector():
      def __init__(self, x: int, y: int):
          self._x, self._y = x, y

      def __add__(self, other):
        pass
    
      def __neg__(self):
        pass

      def __repr__(self):
        pass

For a harder question create a vector object that handles an arbitrary vector dimension. If two vectors of different sizes are added, __add__ should raise a ValueError.

Exercise 3 (Currency) Create a class for working with the currencies AUD, EUR, and JPY in Python.

Implement the __repr__, __gt__, and __add__ magic methods. You will have to find the symbols for dollar, euro, and yen as well as do currency conversions when adding different currencies together.

Use 1 AUD is 0.62 EUR 1 AUD is 79.7 JPY

Currency Starter Code

class Currency():
      def __init__(self, value: float, currency: str) -> None:
          """ <currency> is one of 'AUD', 'EUR', 'JPY'.
          """
          self.value = value
          self.currency = currency

      def __repr__(self) -> str:
        pass

      def __add__(self, other) -> object:
        pass

      def __gt__(self, other) -> bool:
        pass

Exercise 4 (Greeter) Implement the following class

class Greeter():
    _lang_to_hello = {
        "FR": "Bonjour", 
        "AU": "G'Day", 
        "DE": "Hallo", 
        "CN": "Ni Hao"
    }

    def __init__(self, country: str) -> None:
        self._country = country

    def greet(self) -> str:
        return Greeter._lang_to_hello[self._country]

so that it has the following functionality.

a = Greeter("FR")
b = Greeter("AU")
c = Greeter("DE")
d = Greeter("CN")
a.greet()
'Bonjour'
b.greet()
"G'Day"
c.greet()
'Hallo'
d.greet()  
'Ni Hao'

Summary

Classes (or objects) are like functions that maintain their state even after returning. Classes have attributes and methods and provide a public interface through setters and getters for manipulating values considered private to the object.