Skip to content

Week 14 - Classes & Objects

Object Composition and OOP Features in Python

Objectives:

  1. Understand object composition and why it’s central to object*oriented programming.
  2. Learn about special method overriding, particularly __eq__, and the distinction between == (equivalence) and is (identity).
  3. Explore state change in objects through methods like translate and grow that change object attribute values.
  4. Learn Python’s copy module, shallow vs. deep copies, and when to use each.
  5. Understand polymorphism and its practical benefits in object*oriented programming.

Introduction to Object Composition

Definition and Benefits:

  • Object Composition: A design principle where objects are made up of other objects, allowing more flexible and modular designs.
    • Example: A Line is composed of two Point objects.
    • Example: A Rectangle is composed of four Line objects.
    • Why Composition is Preferred: It encourages reuse and models real-world hierarchies better than inheritance in most cases (inheritance is a topic for next week).

Example: Point and Line Classes

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Point:
    """Represents a point in 2D space."""
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __str__(self):
        return f'Point({self.x}, {self.y})'

    def translate(self, dx, dy):
        self.x += dx
        self.y += dy

class Line:
    """Represents a line segment defined by two points."""
    def __init__(self, p1:Point, p2:Point):
        self.p1 = p1
        self.p2 = p2

    def __str__(self):
        return f'Line({self.p1}, {self.p2})'
Discussion: As you can see, we have a Line composed of 2 Point objects, let’s see how we’d instantiate this (PyCharm Activity).


Special Method Overriding and Equivalence vs. Identity

Special Method __eq__:

  • Purpose: Redefines how the == operator works for objects.
  • Example:
    1
    2
    3
    4
    class Point:
        ...
        def __eq__(self, other):
            return self.x == other.x and self.y == other.y
    
  • As you can see, instead of testing if the other object has the same identity, it checks the equivalency of each object’s attributes (Note: a subset of attributes is often used).

Identity vs. Equivalence:

  • is: Checks object identity (memory address or object unique-id in some languages).
  • ==: By default checks identity since its inherited from Python’s base object, but it is usually Overloaded to check equivalence as defined by __eq__ (see discussion above).

Let’s see a Demonstration:

1
2
3
4
5
6
p1 = Point(100, 200)
p2 = Point(100, 200)
p3 = p1
print(p1 == p2)  # True (equivalence via __eq__)
print(p1 is p2)  # False (different objects)
print(p1 is p3)  # True (they point to the same object)
* As you can see the is operator verifies object identity while the equivalance operator checks for attribute equivalence. Try removing the overload and see what happens (PyCharm Activity)


Object State Change with Mutable Attributes

UUsing Methods to Change Object State:

  • Methods are the actions objects perform or that are performed on objects (hence why we say English verbs become methods), but how do methods affect the objects?
  • Each object has its own vaules for the attributes / instance variables defined in a class, thus methods affect objects by changing these attribute values.
  • translate: Modify x and y coordinates to move a Rectangle object.
  • grow: Adjust width and height attributes to increase or decrease the size of a Rectangle object..

Example with Rectangle:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Rectangle:
    def __init__(self, width, height, corner):
        self.width = width
        self.height = height
        self.corner = corner

    def translate(self, dx, dy):
        self.corner.translate(dx, dy)

    def grow(self, dwidth, dheight):
        self.width += dwidth
        self.height += dheight

Discussion: Mutating an object via methods that change attribute values is exactly what we want to do in OOP, however we have to be careful when ojbects use composition. For example, in hte Rectangle above, the self.corner attribute is a composed object of type Point. If we copy our rectangle with the copy module, the self.corner memory address get’s copied to the new Rectangle object, so manipulating self.corner via translate will actually impact both Rectangle objects. Let’s see this in action (PyCharm Activity).

We’ll try to clarify this further in the next section.


Copy Module and Deep Copying

Shallow Copy:

  • Shallow copies made by the copy module’s copy function only copies an object, but not the object’s the object composes or references.
    1
    2
    3
    4
    5
    6
    from copy import copy
    
    box1 = Rectangle(100, 50, Point(10, 10))
    box2 = copy(box1)
    box2.corner.translate(5, 5)
    print(box1.corner)  # Affected due to shared reference
    
  • See how the shallow copy didn’t copy the self.corner point?
  • See how the SAME Point object was translated for both box1 and box2?
  • This is referred to as an unintended side effect and can easily happen in OOP languages if the programmer is not careful due to mutable objects.

Deep Copy:

  • Now a deep copy creates a fully independent object and copies the objeca AND any composed objects. It even copies objects composed in child objects.
  • Remember the memoization shortcut we learned in the recursion chapter? That is often used in these types of copies to make object copies faster and allows us to know when to handle copy loops (AKA cyclic object dependencies).
    1
    2
    3
    4
    5
    from copy import deepcopy
    
    box3 = deepcopy(box1)
    box3.corner.translate(10, 10)
    print(box1.corner)  # Unaffected
    
  • Now the corner Point refers to a completely different object, so there are not unintended side-effects.

Polymorphism

Definition and Benefits:

  • Polymorphism: Same interface, different behaviors for objects.
    • Python doesn’t have the concept of an interface so by this we mean 2 classes have the SAME Methods
    • Python does have Abstract classes, which is similar to an interface, but that’s a topic of CIS-18
  • How is Polymorphism Useful: It simplifies code by allowing different object types to be treated in a uniform way. How? Because if the objects all have the same methods and arguments, you can treat them as if they are the same type of object, but when you call those methods, the actions performed by the methods can be drastically different.

Example: The draw Method

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Rectangle:
    ...
    def draw(self):
        print(f'Drawing rectangle at {self.corner} with width {self.width} and height {self.height}')

class Line:
    ...
    def draw(self):
        print(f'Drawing line from {self.p1} to {self.p2}')

shapes = [Rectangle(50, 30, Point(10, 10)), Line(Point(0, 0), Point(10, 0))]
for shape in shapes:
    shape.draw()
  • In this example, both the Rectanlge and the Line classes havre a draw method, so we can put both into a list, loop over them, and know that the draw method will be accessible no matter which object we’re referencing in our loop. Still, one draws a line when called, and one draws a rectangle! POLYMORPHISM!

Interactive Exercises

  1. Equivalence vs. Identity:
    • Create a class named Dog with attributes breed, name, age and size
    • Override the str method to return an f-string formatting all the attributes.
    • Override the eq method to test if breed, age, and size are all equivalent.
    • Create two objects of type Dog. Test your objects using is and ==.
  2. Shallow vs. Deep Copy:
    • Now create a class called Size with a string attribute named size which can be set to small, medium, or large
    • Override its str method to return the size attribute.
    • Override its eq method to compare the size attribute to another Size attribute.
    • Now change the attribute of your original Dog class to be of type Size and update your Dog class’ eq method to work with your Size class if necessary?
      • Was it necessary? Why / Why Not?
    • Finally, Experiment with copy and deepcopy to observe behavior differences after copying dog objects and modifying their Size attributes.
  3. Polymorphism:
    • Let’s extend the Line Rectangle exmaple with a Square class.
    • Let’s practice implementing a draw method to demonstrate polymorphism.