OOP Intermediate
Polymorphism
Same Interface, Different Behavior
Your code processes shapes. Each shape has an area() method, but Circle and
Rectangle calculate area differently. Polymorphism lets you call shape.area()
without knowing which specific shape it is - the right method runs automatically.
Duck typing
If it has the right methods, it works.
# Duck Typing - "If it quacks like a duck..."
class Duck:
"""A real duck."""
def quack(self):
return "Quack quack!"
def walk(self):
return "Waddle waddle"
class Person:
"""A person who can imitate a duck."""
def quack(self):
return "I'm pretending to quack!"
def walk(self):
return "Walking on two legs"
class RobotDuck:
"""A mechanical duck toy."""
def quack(self):
return "Electronic quack!"
def walk(self):
return "Mechanical walking..."
class Dog:
"""A dog cannot quack."""
def bark(self):
return "Woof!"
def walk(self):
return "Running on four legs"
# Function that uses duck typing
def make_it_quack(duck_like_thing):
"""Works with anything that has quack() method."""
return duck_like_thing.quack()
def duck_show(duck_like_thing):
"""Demonstrate both quack and walk."""
print(f" Quack: {duck_like_thing.quack()}")
print(f" Walk: {duck_like_thing.walk()}")
print("=== Duck Typing ===\n")
print("The principle:")
print("'If it walks like a duck and quacks like a duck, it's a duck!'")
print("(Actual type doesn't matter - behavior does)")
# Create objects of different types
real_duck = Duck()
person = Person()
robot = RobotDuck()
print("\n--- Making things quack ---")
# All can quack!
quackers = [real_duck, person, robot]
for obj in quackers:
print(f"{type(obj).__name__}: {make_it_quack(obj)}")
print("\n--- Full duck demonstration ---")
for obj in quackers:
print(f"\n{type(obj).__name__}:")
duck_show(obj)
print("\n--- What about Dog? ---")
dog = Dog()
print(f"Dog can walk: {dog.walk()}")
print("But if we try to make Dog quack...")
try:
make_it_quack(dog)
except AttributeError as e:
print(f"Error: {e}")
print("Dog doesn't have quack() method!")
print("\n=== Duck Typing Rules ===")
print("""
1. No formal interface or inheritance required
2. Object just needs the right methods
3. Type is checked at runtime (not compile time)
4. AttributeError if method missing
Advantages:
- Flexible and dynamic
- Easy to extend
- No type hierarchy needed
Disadvantages:
- Errors only at runtime
- IDE may not catch mistakes
- Need good documentation/naming
""")
No interface declaration needed. If it has speak(), you can call speak().
Method override polymorphism
Same method name, different behavior per class.
# Polymorphism Through Method Overriding
class Animal:
"""Base class for animals."""
def __init__(self, name):
self.name = name
def speak(self):
"""Default speak - to be overridden."""
return f"{self.name} makes a sound"
def describe(self):
return f"{self.name} is an animal"
class Dog(Animal):
"""Dog overrides speak."""
def speak(self):
return f"{self.name} says: Woof woof!"
def describe(self):
return f"{self.name} is a loyal dog"
class Cat(Animal):
"""Cat overrides speak."""
def speak(self):
return f"{self.name} says: Meow~"
def describe(self):
return f"{self.name} is an independent cat"
class Cow(Animal):
"""Cow overrides speak."""
def speak(self):
return f"{self.name} says: Moo!"
def describe(self):
return f"{self.name} is a gentle cow"
class Fish(Animal):
"""Fish - some methods make less sense."""
def speak(self):
return f"{self.name} says: ... (bubbles)"
def describe(self):
return f"{self.name} is a quiet fish"
# Polymorphic function
def animal_concert(animals):
"""Make all animals speak - polymorphic behavior!"""
print("** Animal Concert **")
print("-" * 30)
for animal in animals:
print(animal.speak())
print("-" * 30)
def animal_roll_call(animals):
"""Describe all animals."""
print("\nRoll Call:")
for animal in animals:
print(f" - {animal.describe()}")
print("=== Polymorphism via Method Overriding ===\n")
# Create different animals
animals = [
Dog("Buddy"),
Cat("Whiskers"),
Cow("Bessie"),
Fish("Nemo"),
Dog("Max"),
Cat("Mittens"),
]
# Same method name, different behavior
animal_concert(animals)
animal_roll_call(animals)
# The beauty of polymorphism
print("\n--- Processing uniformly ---")
for animal in animals:
# All animals have same interface
print(f"{animal.name}: ", end="")
print(animal.speak())
# Adding new animal type - no function changes needed!
print("\n--- Adding new animal type ---")
class Duck(Animal):
def speak(self):
return f"{self.name} says: Quack quack!"
def describe(self):
return f"{self.name} is a happy duck"
# Works immediately with existing functions
duck = Duck("Donald")
print(f"New animal: {duck.describe()}")
print(f"In concert: {duck.speak()}")
# Add to list and process
animals.append(duck)
print(f"\nTotal animals now: {len(animals)}")
print("\n=== Key Points ===")
print("""
1. Parent class defines the method (speak)
2. Each child overrides with specific behavior
3. Functions work with parent type (Animal)
4. Actual behavior depends on runtime type
5. New types work without changing functions
This is the essence of polymorphism:
- One interface (Animal.speak)
- Many implementations (Dog, Cat, Cow, etc.)
- Uniform handling (animal_concert works for all)
""")
Parent defines method. Each child overrides with its own implementation.
Common interface
Design classes to share method names.
# Designing Classes with Common Interface
class Drawable:
"""
Classes that can be drawn share this interface.
In Python, this is just a convention - no formal interface.
"""
def draw(self):
"""Draw the object - override this!"""
raise NotImplementedError("Subclasses must implement draw()")
def get_bounds(self):
"""Return bounding box - override this!"""
raise NotImplementedError("Subclasses must implement get_bounds()")
class Circle:
"""Circle implements Drawable interface."""
def __init__(self, x, y, radius):
self.x = x
self.y = y
self.radius = radius
def draw(self):
return f"Drawing circle at ({self.x}, {self.y}) with radius {self.radius}"
def get_bounds(self):
return (self.x - self.radius, self.y - self.radius,
self.x + self.radius, self.y + self.radius)
class Rectangle:
"""Rectangle implements Drawable interface."""
def __init__(self, x, y, width, height):
self.x = x
self.y = y
self.width = width
self.height = height
def draw(self):
return f"Drawing rectangle at ({self.x}, {self.y}), size {self.width}x{self.height}"
def get_bounds(self):
return (self.x, self.y, self.x + self.width, self.y + self.height)
class Line:
"""Line implements Drawable interface."""
def __init__(self, x1, y1, x2, y2):
self.x1, self.y1 = x1, y1
self.x2, self.y2 = x2, y2
def draw(self):
return f"Drawing line from ({self.x1}, {self.y1}) to ({self.x2}, {self.y2})"
def get_bounds(self):
return (min(self.x1, self.x2), min(self.y1, self.y2),
max(self.x1, self.x2), max(self.y1, self.y2))
class Text:
"""Text implements Drawable interface."""
def __init__(self, x, y, content, font_size=12):
self.x = x
self.y = y
self.content = content
self.font_size = font_size
def draw(self):
return f"Drawing text '{self.content}' at ({self.x}, {self.y}), size {self.font_size}"
def get_bounds(self):
# Approximate bounds based on content length
width = len(self.content) * self.font_size * 0.6
height = self.font_size
return (self.x, self.y, self.x + width, self.y + height)
# Canvas that uses the common interface
class Canvas:
"""Canvas that holds and renders drawable objects."""
def __init__(self, name):
self.name = name
self.objects = []
def add(self, drawable):
"""Add any object with draw() method."""
self.objects.append(drawable)
def render_all(self):
"""Render all objects - polymorphic!"""
print(f"\n🖼️ Rendering {self.name}:")
print("-" * 40)
for obj in self.objects:
print(f" {obj.draw()}")
print("-" * 40)
def get_total_bounds(self):
"""Calculate bounding box for all objects."""
if not self.objects:
return (0, 0, 0, 0)
bounds_list = [obj.get_bounds() for obj in self.objects]
min_x = min(b[0] for b in bounds_list)
min_y = min(b[1] for b in bounds_list)
max_x = max(b[2] for b in bounds_list)
max_y = max(b[3] for b in bounds_list)
return (min_x, min_y, max_x, max_y)
print("=== Common Interface Design ===\n")
# Create drawable objects
circle = Circle(100, 100, 50)
rect = Rectangle(200, 50, 80, 60)
line = Line(0, 0, 150, 150)
text = Text(50, 200, "Hello World", 14)
# Create canvas and add objects
canvas = Canvas("My Drawing")
canvas.add(circle)
canvas.add(rect)
canvas.add(line)
canvas.add(text)
# Render - polymorphism in action!
canvas.render_all()
# Get bounds - also polymorphic
total_bounds = canvas.get_total_bounds()
print(f"\nTotal canvas bounds: {total_bounds}")
# Demonstrate individual bounds
print("\nIndividual bounds:")
for obj in canvas.objects:
print(f" {type(obj).__name__}: {obj.get_bounds()}")
# Add more objects dynamically
print("\n--- Adding more objects ---")
canvas.add(Circle(300, 300, 25))
canvas.add(Text(10, 10, "New!", 20))
canvas.render_all()
print("\n=== Interface Design Best Practices ===")
print("""
1. Define clear method signatures
- draw() for all drawable objects
- get_bounds() for spatial info
2. Document the expected interface
- Use docstrings
- Consider type hints
3. Use NotImplementedError for base class
- Signals "must override"
- Fails fast if forgotten
4. Keep interface minimal
- Only essential methods
- Easy to implement
5. Test with duck typing
- If it has draw() and get_bounds()
- It works with Canvas!
""")
All shapes have area() and perimeter(). Code works with any shape.
Polymorphic functions
Write functions that work with any compatible object.
# Polymorphic Functions
class Dog:
def __init__(self, name):
self.name = name
def speak(self):
return "Woof!"
def __len__(self):
return len(self.name)
class Cat:
def __init__(self, name):
self.name = name
def speak(self):
return "Meow!"
def __len__(self):
return len(self.name)
class Robot:
def __init__(self, model):
self.model = model
def speak(self):
return "Beep boop!"
def __len__(self):
return len(self.model)
# Polymorphic function - works with any object that has speak()
def make_speak(speaker):
"""
Works with any object that has a speak() method.
This is polymorphism via duck typing.
"""
return speaker.speak()
# Function with type-specific logic
def describe_speaker(speaker):
"""
Polymorphic function that also handles type differences.
"""
sound = speaker.speak()
# Can use hasattr to check for attributes
if hasattr(speaker, 'name'):
identifier = speaker.name
elif hasattr(speaker, 'model'):
identifier = speaker.model
else:
identifier = "Unknown"
return f"{identifier} says: {sound}"
# Function using len() - built-in polymorphism
def show_length(obj):
"""
len() is polymorphic - works with many types.
Requires __len__ method.
"""
return f"Length of {type(obj).__name__}: {len(obj)}"
# Function accepting multiple types with shared behavior
def make_chorus(speakers, times=2):
"""
Make all speakers speak multiple times.
Works with any list of objects with speak() method.
"""
print("\n** Chorus Time! **")
for _ in range(times):
for speaker in speakers:
print(f" {make_speak(speaker)}")
print("---")
# Higher-order polymorphic function
def process_speakers(speakers, processor):
"""
Apply any processor function to speakers.
Both the speakers and processor are polymorphic!
"""
results = []
for speaker in speakers:
result = processor(speaker)
results.append(result)
return results
print("=== Polymorphic Functions ===\n")
# Create different objects
dog = Dog("Buddy")
cat = Cat("Whiskers")
robot = Robot("R2D2")
speakers =
# Basic polymorphic function
print("--- make_speak (basic polymorphism) ---")
for s in speakers:
print(f" {make_speak(s)}")
# Polymorphic with type handling
print("\n--- describe_speaker (with type handling) ---")
for s in speakers:
print(f" {describe_speaker(s)}")
# Built-in len() polymorphism
print("\n--- show_length (built-in len polymorphism) ---")
for s in speakers:
print(f" {show_length(s)}")
# Also works with built-in types!
print("\n Also works with built-in types:")
print(f" {show_length('Hello')}")
print(f" {show_length([1, 2, 3])}")
print(f" {show_length({'a': 1, 'b': 2})}")
# Chorus function
make_chorus(speakers)
# Higher-order function
print("\n--- process_speakers (higher-order) ---")
# Different processors
def loud_speak(s):
return s.speak().upper() + "!!"
def count_speak(s):
return f"{s.speak()} ({len(s.speak())} chars)"
print("Loud processor:")
results = process_speakers(speakers, loud_speak)
for r in results:
print(f" {r}")
print("\nCount processor:")
results = process_speakers(speakers, count_speak)
for r in results:
print(f" {r}")
print("\n=== Polymorphic Function Benefits ===")
print("""
1. Reusable: One function works with many types
2. Flexible: Easy to add new types
3. Clean: No type-checking spaghetti
4. Pythonic: Embraces duck typing
Best practices:
- Document expected interface
- Use meaningful parameter names
- Handle missing methods gracefully (hasattr)
- Consider type hints for documentation
""")
# Polymorphic Functions
class Dog:
def __init__(self, name):
self.name = name
def speak(self):
return "Woof!"
def __len__(self):
return len(self.name)
class Cat:
def __init__(self, name):
self.name = name
def speak(self):
return "Meow!"
def __len__(self):
return len(self.name)
class Robot:
def __init__(self, model):
self.model = model
def speak(self):
return "Beep boop!"
def __len__(self):
return len(self.model)
# Polymorphic function - works with any object that has speak()
def make_speak(speaker):
"""
Works with any object that has a speak() method.
This is polymorphism via duck typing.
"""
return speaker.speak()
# Function with type-specific logic
def describe_speaker(speaker):
"""
Polymorphic function that also handles type differences.
"""
sound = speaker.speak()
# Can use hasattr to check for attributes
if hasattr(speaker, 'name'):
identifier = speaker.name
elif hasattr(speaker, 'model'):
identifier = speaker.model
else:
identifier = "Unknown"
return f"{identifier} says: {sound}"
# Function using len() - built-in polymorphism
def show_length(obj):
"""
len() is polymorphic - works with many types.
Requires __len__ method.
"""
return f"Length of {type(obj).__name__}: {len(obj)}"
# Function accepting multiple types with shared behavior
def make_chorus(speakers, times=2):
"""
Make all speakers speak multiple times.
Works with any list of objects with speak() method.
"""
print("\n** Chorus Time! **")
for _ in range(times):
for speaker in speakers:
print(f" {make_speak(speaker)}")
print("---")
# Higher-order polymorphic function
def process_speakers(speakers, processor):
"""
Apply any processor function to speakers.
Both the speakers and processor are polymorphic!
"""
results = []
for speaker in speakers:
result = processor(speaker)
results.append(result)
return results
print("=== Polymorphic Functions ===\n")
# Create different objects
dog = Dog("Buddy")
cat = Cat("Whiskers")
robot = Robot("R2D2")
speakers =
# Basic polymorphic function
print("--- make_speak (basic polymorphism) ---")
for s in speakers:
print(f" {make_speak(s)}")
# Polymorphic with type handling
print("\n--- describe_speaker (with type handling) ---")
for s in speakers:
print(f" {describe_speaker(s)}")
# Built-in len() polymorphism
print("\n--- show_length (built-in len polymorphism) ---")
for s in speakers:
print(f" {show_length(s)}")
# Also works with built-in types!
print("\n Also works with built-in types:")
print(f" {show_length('Hello')}")
print(f" {show_length([1, 2, 3])}")
print(f" {show_length({'a': 1, 'b': 2})}")
# Chorus function
make_chorus(speakers)
# Higher-order function
print("\n--- process_speakers (higher-order) ---")
# Different processors
def loud_speak(s):
return s.speak().upper() + "!!"
def count_speak(s):
return f"{s.speak()} ({len(s.speak())} chars)"
print("Loud processor:")
results = process_speakers(speakers, loud_speak)
for r in results:
print(f" {r}")
print("\nCount processor:")
results = process_speakers(speakers, count_speak)
for r in results:
print(f" {r}")
print("\n=== Polymorphic Function Benefits ===")
print("""
1. Reusable: One function works with many types
2. Flexible: Easy to add new types
3. Clean: No type-checking spaghetti
4. Pythonic: Embraces duck typing
Best practices:
- Document expected interface
- Use meaningful parameter names
- Handle missing methods gracefully (hasattr)
- Consider type hints for documentation
""")
# Polymorphic Functions
class Dog:
def __init__(self, name):
self.name = name
def speak(self):
return "Woof!"
def __len__(self):
return len(self.name)
class Cat:
def __init__(self, name):
self.name = name
def speak(self):
return "Meow!"
def __len__(self):
return len(self.name)
class Robot:
def __init__(self, model):
self.model = model
def speak(self):
return "Beep boop!"
def __len__(self):
return len(self.model)
# Polymorphic function - works with any object that has speak()
def make_speak(speaker):
"""
Works with any object that has a speak() method.
This is polymorphism via duck typing.
"""
return speaker.speak()
# Function with type-specific logic
def describe_speaker(speaker):
"""
Polymorphic function that also handles type differences.
"""
sound = speaker.speak()
# Can use hasattr to check for attributes
if hasattr(speaker, 'name'):
identifier = speaker.name
elif hasattr(speaker, 'model'):
identifier = speaker.model
else:
identifier = "Unknown"
return f"{identifier} says: {sound}"
# Function using len() - built-in polymorphism
def show_length(obj):
"""
len() is polymorphic - works with many types.
Requires __len__ method.
"""
return f"Length of {type(obj).__name__}: {len(obj)}"
# Function accepting multiple types with shared behavior
def make_chorus(speakers, times=2):
"""
Make all speakers speak multiple times.
Works with any list of objects with speak() method.
"""
print("\n** Chorus Time! **")
for _ in range(times):
for speaker in speakers:
print(f" {make_speak(speaker)}")
print("---")
# Higher-order polymorphic function
def process_speakers(speakers, processor):
"""
Apply any processor function to speakers.
Both the speakers and processor are polymorphic!
"""
results = []
for speaker in speakers:
result = processor(speaker)
results.append(result)
return results
print("=== Polymorphic Functions ===\n")
# Create different objects
dog = Dog("Buddy")
cat = Cat("Whiskers")
robot = Robot("R2D2")
speakers =
# Basic polymorphic function
print("--- make_speak (basic polymorphism) ---")
for s in speakers:
print(f" {make_speak(s)}")
# Polymorphic with type handling
print("\n--- describe_speaker (with type handling) ---")
for s in speakers:
print(f" {describe_speaker(s)}")
# Built-in len() polymorphism
print("\n--- show_length (built-in len polymorphism) ---")
for s in speakers:
print(f" {show_length(s)}")
# Also works with built-in types!
print("\n Also works with built-in types:")
print(f" {show_length('Hello')}")
print(f" {show_length([1, 2, 3])}")
print(f" {show_length({'a': 1, 'b': 2})}")
# Chorus function
make_chorus(speakers)
# Higher-order function
print("\n--- process_speakers (higher-order) ---")
# Different processors
def loud_speak(s):
return s.speak().upper() + "!!"
def count_speak(s):
return f"{s.speak()} ({len(s.speak())} chars)"
print("Loud processor:")
results = process_speakers(speakers, loud_speak)
for r in results:
print(f" {r}")
print("\nCount processor:")
results = process_speakers(speakers, count_speak)
for r in results:
print(f" {r}")
print("\n=== Polymorphic Function Benefits ===")
print("""
1. Reusable: One function works with many types
2. Flexible: Easy to add new types
3. Clean: No type-checking spaghetti
4. Pythonic: Embraces duck typing
Best practices:
- Document expected interface
- Use meaningful parameter names
- Handle missing methods gracefully (hasattr)
- Consider type hints for documentation
""")
Function accepts anything with required methods. Very flexible.
Operator overloading
Polymorphism for operators like +, -, ==.
# Polymorphism via Operator Overloading
class Vector:
"""2D Vector with operator overloading."""
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Vector({self.x}, {self.y})"
def __add__(self, other):
"""v1 + v2"""
return Vector(self.x + other.x, self.y + other.y)
def __sub__(self, other):
"""v1 - v2"""
return Vector(self.x - other.x, self.y - other.y)
def __mul__(self, scalar):
"""v * scalar"""
return Vector(self.x * scalar, self.y * scalar)
def __rmul__(self, scalar):
"""scalar * v (reversed)"""
return self.__mul__(scalar)
def __eq__(self, other):
"""v1 == v2"""
return self.x == other.x and self.y == other.y
def __abs__(self):
"""abs(v) - magnitude"""
return (self.x ** 2 + self.y ** 2) ** 0.5
class Money:
"""Money class with operator overloading."""
def __init__(self, dollars, cents=0):
total_cents = dollars * 100 + cents
self.dollars = total_cents // 100
self.cents = total_cents % 100
def __repr__(self):
return f"Money({self.dollars}, {self.cents})"
def __str__(self):
return f"${self.dollars}.{self.cents:02d}"
def __add__(self, other):
"""m1 + m2"""
total_cents = (self.dollars * 100 + self.cents +
other.dollars * 100 + other.cents)
return Money(total_cents // 100, total_cents % 100)
def __sub__(self, other):
"""m1 - m2"""
total_cents = (self.dollars * 100 + self.cents -
other.dollars * 100 - other.cents)
return Money(total_cents // 100, total_cents % 100)
def __mul__(self, factor):
"""m * factor"""
total_cents = int((self.dollars * 100 + self.cents) * factor)
return Money(total_cents // 100, total_cents % 100)
def __rmul__(self, factor):
return self.__mul__(factor)
def __lt__(self, other):
"""m1 < m2"""
return (self.dollars * 100 + self.cents) < (other.dollars * 100 + other.cents)
def __le__(self, other):
"""m1 <= m2"""
return (self.dollars * 100 + self.cents) <= (other.dollars * 100 + other.cents)
def __eq__(self, other):
"""m1 == m2"""
return self.dollars == other.dollars and self.cents == other.cents
class StringList:
"""List-like class with operator overloading."""
def __init__(self, *items):
self.items = list(items)
def __repr__(self):
return f"StringList{tuple(self.items)}"
def __add__(self, other):
"""Concatenate two StringLists"""
return StringList(*(self.items + other.items))
def __len__(self):
"""len(sl)"""
return len(self.items)
def __getitem__(self, index):
"""sl[index]"""
return self.items[index]
def __contains__(self, item):
"""item in sl"""
return item in self.items
def __iter__(self):
"""for item in sl"""
return iter(self.items)
print("=== Operator Overloading ===\n")
# Vector operations
print("--- Vector Operations ---")
v1 = Vector(3, 4)
v2 = Vector(1, 2)
print(f"v1 = {v1}")
print(f"v2 = {v2}")
print(f"v1 + v2 = {v1 + v2}")
print(f"v1 - v2 = {v1 - v2}")
print(f"v1 * 2 = {v1 * 2}")
print(f"3 * v2 = {3 * v2}")
print(f"|v1| = {abs(v1):.2f}")
print(f"v1 == v2: {v1 == v2}")
print(f"v1 == Vector(3, 4): {v1 == Vector(3, 4)}")
# Money operations
print("\n--- Money Operations ---")
price = Money(19, 99)
tax = Money(1, 60)
discount = Money(5, 0)
print(f"Price: {price}")
print(f"Tax: {tax}")
print(f"Discount: {discount}")
total = price + tax
print(f"Price + Tax = {total}")
final = total - discount
print(f"After discount = {final}")
doubled = price * 2
print(f"Price * 2 = {doubled}")
print(f"Tax < Discount: {tax < discount}")
print(f"Price == Money(19, 99): {price == Money(19, 99)}")
# StringList operations
print("\n--- StringList Operations ---")
fruits = StringList("apple", "banana")
veggies = StringList("carrot", "broccoli")
print(f"fruits = {fruits}")
print(f"veggies = {veggies}")
combined = fruits + veggies
print(f"combined = {combined}")
print(f"len(combined) = {len(combined)}")
print(f"combined[0] = {combined[0]}")
print(f"'apple' in combined: {'apple' in combined}")
print("Iterating:")
for item in combined:
print(f" - {item}")
print("\n=== Common Operators ===")
print("""
Arithmetic:
__add__(self, other) +
__sub__(self, other) -
__mul__(self, other) *
__truediv__(self, other) /
__rmul__(self, other) reversed * (scalar * obj)
Comparison:
__eq__(self, other) ==
__ne__(self, other) !=
__lt__(self, other) <
__le__(self, other) <=
__gt__(self, other) >
__ge__(self, other) >=
Container:
__len__(self) len(obj)
__getitem__(self, key) obj[key]
__contains__(self, item) item in obj
__iter__(self) for x in obj
Other:
__repr__(self) repr(obj), print in shell
__str__(self) str(obj), print()
__abs__(self) abs(obj)
__bool__(self) bool(obj), if obj
""")
Define __add__, __eq__, etc. to make operators work with your class.
Exercise: practical.py
Build a payment system with polymorphic processors