An object can contain references to objects of other classes.
What examples of composition are in an animal conservatory?
An instance variable can refer to another instance.
We can add this method to the base Animal class
that adds a mate instance variable:
class Animal:
def mate_with(self, other):
if other is not self and other.species_name == self.species_name:
self.mate = other
other.mate = self
How would we call that method?
mr_wabbit = Rabbit("Mister Wabbit", 3)
jane_doe = Rabbit("Jane Doe", 2)
mr_wabbit.mate_with(jane_doe)
An instance variable can also store a list of instances.
We can add this method to the Rabbit class
that adds a babies instance variable.
class Rabbit(Animal):
def reproduce_like_rabbits(self):
if self.mate is None:
print("oh no! better go on ZoOkCupid")
return
self.babies = []
for _ in range(0, self.num_in_litter):
self.babies.append(Rabbit("bunny", 0))
How would we call that function?
mr_wabbit = Rabbit("Mister Wabbit", 3)
jane_doe = Rabbit("Jane Doe", 2)
mr_wabbit.mate_with(jane_doe)
jane_doe.reproduce_like_rabbits()
If all instances implement a method with the same function signature, a program can rely on that method across instances of different subclasses.
def partytime(animals):
"""Assuming ANIMALS is a list of Animals, cause each
to interact with all the others exactly once."""
for i in range(len(animals)):
for j in range(i + 1, len(animals)):
animals[i].interact_with(animals[j])
How would we call that function?
jane_doe = Rabbit("Jane Doe", 2)
scar = Lion("Scar", 12)
elly = Elephant("Elly", 5)
pandy = Panda("PandeyBear", 4)
partytime([jane_doe, scar, elly, pandy])
Inheritance is best for representing "is-a" relationships
Composition is best for representing "has-a" relationships
What are the objects in this code?
class Lamb:
species_name = "Lamb"
scientific_name = "Ovis aries"
def __init__(self, name):
self.name = name
def play(self):
self.happy = True
lamb = Lamb("Lil")
owner = "Mary"
had_a_lamb = True
fleece = {"color": "white", "fluffiness": 100}
kids_at_school = ["Billy", "Tilly", "Jilly"]
day = 1
lamb, owner, had_a_lamb, fleece,
kids_at_school, day, etc.
We can prove it by checking object.__class__.__bases__, which reports the base class(es) of the object's class.
All the built-in types inherit from object:
If all the built-in types and user classes inherit from object,
what are they inheriting?
Just ask dir(), a built-in function that returns
a list of all the "interesting" attributes on an object.
dir(object)
__repr__, __str__, __format____eq__, __ge__, __gt__, __le__, __lt__, __ne____bases__, __class__, __new__, __init__,
__init_subclass__, __subclasshook__, __setattr__, __delattr__, __getattribute____dir__, __hash__, __module__, __reduce__, __reduce_ex__Python calls these methods behind these scenes, so we are often not aware
when the "dunder" methods are being called.
💡 Let us become enlightened! 💡
The __str__ method returns a human readable
string representation of an object.
from fractions import Fraction
one_third = 1/3
one_half = Fraction(1, 2)
float.__str__(one_third) # '0.3333333333333333'
Fraction.__str__(one_half) # '1/2'
The __str__ method is used in multiple places by Python: print() function,
str() constructor, f-strings, and more.
from fractions import Fraction
one_third = 1/3
one_half = Fraction(1, 2)
print(one_third) # '0.3333333333333333'
print(one_half) # '1/2'
str(one_third) # '0.3333333333333333'
str(one_half) # '1/2'
f"{one_half} > {one_third}" # '1/2 > 0.3333333333333333'
When making custom classes, we can override __str__
to define our human readable string representation.
class Lamb:
species_name = "Lamb"
scientific_name = "Ovis aries"
def __init__(self, name):
self.name = name
def __str__(self):
return "Lamb named " + self.name
lil = Lamb("Lil lamb")
str(lil)
print(lil)
The __repr__ method returns
a string that would evaluate to an object with the same values.
from fractions import Fraction
one_half = Fraction(1, 2)
Fraction.__repr__(one_half) # 'Fraction(1, 2)'
If implemented correctly, calling eval()
on the result should return back that same-valued object.
another_half = eval(Fraction.__repr__(one_half))
The __repr__ method is used multiple places by Python:
when repr(object) is called
and when displaying an object in an interactive Python session.
from fractions import Fraction
one_third = 1/3
one_half = Fraction(1, 2)
one_third
one_half
repr(one_third)
repr(one_half)
When making custom classes, we can override __repr__
to return a more appropriate Python representation.
class Lamb:
species_name = "Lamb"
scientific_name = "Ovis aries"
def __init__(self, name):
self.name = name
def __str__(self):
return "Lamb named " + self.name
def __repr__(self):
return f"Lamb({repr(self.name)})"
lil = Lamb("Lil lamb")
repr(lil)
lil
When the repr(obj) function is called:
ClassName.__repr__ method if it exists.ClassName.__repr__ does not exist,
Python will look up the chain of parent classes until it finds
one with __repr__ defined.object.__repr__ will be called.When the str(obj) class constructor is called:
ClassName.__str__ method if it exists.__str__ method is found on that class,
Python calls repr() on the object instead.
Special methods have built-in behavior. Special method names always start and end with double underscores.
| Name | Behavior |
|---|---|
__init__
| Method invoked automatically when an object is constructed |
__repr__
| Method invoked to display an object as a Python expression |
__str__
| Method invoked to stringify an object |
__add__
| Method invoked to add one object to another |
__bool__
| Method invoked to convert an object to True or False |
__float__
| Method invoked to convert an object to a float (real number) |
zero = 0
one = 1
two = 2
| Standard approach | Dunder equivalent |
|---|---|
|
|
|
|
|
|
Consider the following class:
from math import gcd
class Rational:
def __init__(self, numerator, denominator):
g = gcd(numerator, denominator)
self.numer = numerator // g
self.denom = denominator // g
def __str__(self):
return f"{self.numer}/{self.denom}"
def __repr__(self):
return f"Rational({self.numer}, {self.denom})"
Will this work?
Rational(1, 2) + Rational(3, 4)
🚫 TypeError: unsupported operand type(s) for +: 'Rational' and 'Rational'
We can make instances of custom classes addable by defining the __add__ method:
class Rational:
def __init__(self, numerator, denominator):
g = gcd(numerator, denominator)
self.numer = numerator // g
self.denom = denominator // g
def __add__(self, other):
new_numer = self.numer * other.denom + other.numer * self.denom
new_denom = self.denom * other.denom
return Rational(new_numer, new_denom)
# The rest...
Now try...
Rational(1, 2) + Rational(3, 4)
Polymorphic function: A function that applies to many (poly) different forms (morph) of data
str and repr are both polymorphic; they apply to any object.
repr(1/3) # '0.3333333333333333'
repr(Rational(1, 3)) # 'Rational(1, 3)'
str(1/3) # '0.3333333333333333'
str(Rational(1, 3)) # '1/3'
The class of that object can customize the per-object behavior using __str__
and __repr__.
A generic function can apply to arguments of different types.
def sum_two(a, b):
return a + b
What could a and b be? Anything summable!
The function sum_two is generic in the type of a and b.
def sum_em(items, initial_value):
"""Returns the sum of ITEMS,
starting with a value of INITIAL_VALUE."""
sum = initial_value
for item in items:
sum += item
return sum
What could items be? Any iterable with summable values.
What could initial_value be? Any value that can be summed with the values in iterable.
The function sum_em is generic in the type of items and the type of initial_value.
Another way to make generic functions is to select a behavior based on the type of the argument.
def is_valid_month(month):
if isinstance(month, int):
return month >= 1 and month <= 12
elif isinstance(month, str):
return month in ["January", "February", "March", "April",
"May", "June", "July", "August", "September",
"October", "November", "December"]
return false
What could month be? Either an int or string.
The function is_valid_month is generic in the type of month.
Another way to make generic functions is to coerce an argument into the desired type.
def sum_numbers(nums):
"""Returns the sum of NUMS"""
sum = Rational(0, 0)
for num in nums:
if isinstance(num, int):
num = Rational(num, 1)
sum += num
return sum
What could nums be? Any iterable with ints or Rationals.
The function sum_numbers is generic in the type of nums.