Polymorphism: means having many forms.
In programming, polymorphism means having the same function name (but different signatures) being used for different types.
# len() being used for a string
print(len("geeks"))
# len() being used for a list
print(len([10, 20, 30]))
# The behavior of len() is polymorphic,
# for a string it gives the length of a string
# for a list gives the length of the list
def add(x, y):
return x + y
add(10, 5) # Returns 15
add("hello ", "world") # Returns "hello world"
class Point:
""" Create a new Point, at coordinates x, y """
def __init__(self, x=0, y=0):
""" Create a new point at x, y """
self.x = x
self.y = y
def distance_from_origin(self):
""" Compute my distance from the origin """
# This is just Pythagorus's theorem
return ((self.x ** 2) + (self.y ** 2)) ** 0.5
def to_string(self):
"""Return a string representing the point"""
return "({0}, {1})".format(self.x, self.y)
p = Point(3, 4)
print(p.to_string())
print(p) # Not very helpful
# Prints <__main__.Point object at 0x7f6ee59eecf8>
# print function implicitly calls str() function,
# Let's see how we can fix this
class Point:
""" Create a new Point, at coordinates x, y """
def __init__(self, x=0, y=0):
""" Create a new point at x, y """
self.x = x
self.y = y
def distance_from_origin(self):
""" Compute my distance from the origin """
# This is just Pythagorus's theorem
return ((self.x ** 2) + (self.y ** 2)) ** 0.5
def __str__(self):
"""Return a string representing the point"""
return "({0}, {1})".format(self.x, self.y)
p = Point(3, 4)
print(str(p)) # Prints (3, 4)
print(p) # Prints (3, 4)
as another example of polymorphism, let's revisit == and the 'is' keyword and figure out how Python implements ==
Recall from lecture 14 that:
== is for equivalence
'is' is for testing if references point to the same thing in memory
x = [1,2]
y = [1,2]
x == y # True! Python is checking that they represent the same thing
x is y # False! with "is", Python is checking that x and y reference the same thing
z = x # Make another reference to the object pointed to by x
z is x # True
The 'is' keyword is not user specifiable: references are either the same or they are not.
However, "==" for operators is specifiable _eq_(). Let's look at an example of how this works:
class Point:
""" Create a new Point, at coordinates x, y """
def __init__(self, x=0, y=0):
""" Create a new point at x, y """
self.x = x
self.y = y
p = Point(5, 10)
q = Point(5, 10)
p == q
# False! This is because by default __eq__() on objects works like 'is'
p == p
# True, because p is p
# Let's see how to fix this
# Now let's implement the __eq__
class Point:
""" Create a new Point, at coordinates x, y """
def __init__(self, x=0, y=0):
""" Create a new point at x, y """
self.x = x
self.y = y
def distance_from_origin(self):
""" Compute my distance from the origin """
return ((self.x ** 2) + (self.y ** 2)) ** 0.5
def __str__(self):
"""Return a string representing the point"""
return "({0}, {1})".format(self.x, self.y)
def __eq__(self, p):
""" True if the points have the same x, y, coordinates """
return (self.x, self.y) == (p.x, p.y)
p = Point(5, 10)
q = Point(5, 10)
p == q # True
# We can also do not equals (!= operator)
p != q # False
p == (x, y) # But note, if we try to compare things that are not comparable we'll get a runtime exception
We've seen two examples of polymorphism:
We can alter the behavior of str() and == by implementing, respectively, _str_() and _eq_().
Implementing _eq_() is an example of "operator overloading", polymorphism in which we override the default behavior of an operator, in this case, ==
class Point:
""" Create a new Point, at coordinates x, y """
def __init__(self, x=0, y=0):
""" Create a new point at x, y """
self.x = x
self.y = y
def distance_from_origin(self):
""" Compute my distance from the origin """
return ((self.x ** 2) + (self.y ** 2)) ** 0.5
def __str__(self):
"""Return a string representing the point"""
return "({0}, {1})".format(self.x, self.y)
def __eq__(self, p):
""" True if the points have the same x, y, coordinates """
return (self.x, self.y) == (p.x, p.y)
def __add__(self, p):
""" Allows the addition of two points """ # This implements the + operator
return Point(self.x + p.x, self.y + p.y)
def __mul__(self, p):
""" Allows the multiplication of two points """ # This implement the * operator
return Point(self.x * p.x, self.y * p.y)
p = Point(5, 10)
p2 = Point(2, 3)
print("Add", p + p2) # (5+1, 10 + 3)
print("Multiply", p * p2) # (5*2, 10 * 3)
Let's finish operator overloading by considering the general comparison operators.
Python implements all the difference logical comparison operators and each maps to a specific object function
== implement _eq_()
!= implement _ne_() (this one is optional if you do _eq_())
<= implement _le_()
>= implement _ge_()
< implement _lt_()
> implement _gt_()
class Point:
""" Create a new Point, at coordinates x, y """
def __init__(self, x=0, y=0):
""" Create a new point at x, y """
self.x = x
self.y = y
def distance_from_origin(self):
""" Compute my distance from the origin """
return ((self.x ** 2) + (self.y ** 2)) ** 0.5
def __str__(self):
"""Return a string representing the point"""
return "({0}, {1})".format(self.x, self.y)
def __eq__(self, p):
""" True if the points have the same x, y, coordinates """
return (self.x, self.y) == (p.x, p.y)
def __lt__(self, p):
return (self.x, self.y) < (p.x, p.y)
def __le__(self, p):
return (self.x, self.y) <= (p.x, p.y)
def __gt__(self, p):
return (self.x, self.y) > (p.x, p.y)
def __ge__(self, p):
return (self.x, self.y) >= (p.x, p.y)
p = Point(3, 4)
q = Point(3, 5)
print("p == q", p == q)
print("p != q", p != q)
print("p <= q", p <= q)
print("p < q", p < q)
print("p >= q", p >= q)
print("p > q", p > q)
Python copies the contents of the p1 object's memory to a new location in memory, making copies of its references
p1 is composed of memory representing p1.x and p1.y. p2 is a new location in memory with copies of these references. i.e. p1.x is p2.x and p1.y is p2.y
import copy
p1 = Point(3, 4)
p2 = copy.copy(p1)
p1 == p2
# True
p1 is p2
# False, p2 is a copy of p1 so the memory references are different
p1.x is p2.x
# True! This is true because the references of p1 are copied into p2
p1.y is p2.y # Similarly
# If we reassign the variables in p1, this does not affect the references p2
p1.x = 7
p1.y = 12
print("p1", p1) # p1 (7, 12)
print("p2", p2) # p2 (3, 4)
class Rectangle:
""" A class to manufacture rectangle objects """
def __init__(self, posn, w, h):
""" Initialize rectangle at posn, with width w, height h """
self.corner = posn
# This is an example of composition, where we re-use point
self.width = w
self.height = h
def __str__(self):
return "({0}, {1}, {2})".format(self.corner, self.width, self.height)
# Note how self.corner reuses the __str__ method of Point
box = Rectangle(Point(0, 0), 100, 200)
print("box: ", box) # box: ((0, 0), 100, 200)
r = Rectangle(Point(3, 4), 100, 200)
r2 = copy.copy(r)
print("r", r) # r ((3, 4), 100, 200)
print("r2", r2) # r2 ((3, 4), 100, 200)
r.corner.x = 10
print("after change r", r) # after change r ((10, 4), 100, 200)
print("after change r2", r2) # after change r2 ((10, 4), 100, 200)
# Ack, r.corner is r2.corner, so changes to r.corner change r2.corner
r = Rectangle(Point(3, 4), 100, 200)
r2 = copy.deepcopy(r)
print("r", r) # r ((3, 4), 100, 200)
print("r2", r2) # r2 ((3, 4), 100, 200)
r.corner.x = 10
print("after change r", r) # after change r ((10, 4), 100, 200)
print("after change r2", r2) # after change r2 ((3, 4), 100, 200)
# Now the r2.corner is not r.corner, because the corner object was copied in turn