from __future__ import annotations
class Fraction():
def __init__(self, numer: int, denom: int) -> None:
self._numer = numer
self._denom = denom
def __str__(self) -> str:
return f"{self._numer} / {self._denom}"
def __repr__(self) -> str:
return f"Fraction({self._numer}, {self._denom})"
def __add__(self, other) -> Fraction:
= self._numer, self._denom
a, b = other._numer, other._denom
c, d return Fraction(a*d + c*b, b*d)
Dunder Methods
Introduction to Software Engineering (CSSE 1001)
Magic / Dunder Methods
Overloading built-in functions. Underscores
How underscores are used in python. (List not comprehensive.)
- As anonymous variables like in
for _ in [1, 2, 3]
orx, _, z = (1, 2, 3)
. - For giving special meaning to function and names.
_private
variables.__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}"
= Fraction(2, 3)
p 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})"
= Fraction(2, 3)
p 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"
= self._numer, self._denom
a, b = other._numer, other._denom
c, d return a*d == b*c
= Fraction(4, 6)
p = Fraction(2, 3)
q == q p
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:
= self._numer, self._denom
a, b = other._numer, other._denom
c, d return Fraction(a*d + c*b, b*d)
= Fraction(2, 3)
p = Fraction(1, 2)
q + q p
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():
= 0 # every instance has access to this
_all_clicks
def __init__(self) -> None:
self._clicks = 0
def click(self) -> None:
self._clicks += 1 # access instance variable
+= 1 # access class variable Clicker._all_clicks
= Clicker(); d = Clicker(); e = Clicker()
c # 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.
= Alarm(5)
a 1)
a.wait( a
'Alarm pending.'
2)
a.wait( a
'Alarm pending.'
3) B.wait(
*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
= Vector(1, 2)
x = Vector(3, 4) y
+ y x
<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.
= Greeter("FR")
a = Greeter("AU")
b = Greeter("DE")
c = Greeter("CN") d
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.