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
.