Beginning Programming in Python

Fall 2019

Agenda

  • Polymorphism
    • Polymorphism by reimplementing basic object functionality:
    • Redefining the _str_ method
      • How Python implements ''==" for objects
      • General operator overloading, including comparison operators
    • Copying objects

Polymorphism 

  • 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

Polymorphic behavior of + operator

def add(x, y):
  return x + y

add(10, 5) # Returns 15

add("hello ", "world") # Returns "hello world"

redefining str() funtion(1)

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

redefining str() funtion(2)

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)

How python implements ==

  • 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

Objects equality

  • 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

Objects equality - Fixed

# 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

5 minutes break!

General operator overloading

  • 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, ==

  • We can do the same for other operators, let's see...

Operator overloading

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)

Overloading comparison operators

  • 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)

copying objects

0
 Advanced issues found
 
  • 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)

example

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

Lecture 15 challenge

Questions?

CSE 20 - Lecture 15

By Narges Norouzi

CSE 20 - Lecture 15

  • 1,299