Implementing Operators in your Classes and Hiding Attributes¶

CS 66: Introduction to Computer Science II¶

References for this lecture¶

Problem Solving with Algorithms and Data Structures using Python

Section 1.13: https://runestone.academy/ns/books/published/pythonds/Introduction/ObjectOrientedProgramminginPythonDefiningClasses.html

Section 2.1: https://runestone.academy/ns/books/published/pythonds/ProperClasses/a_proper_python_class.html

Section 2.2 appears to be missing from the book, but we'll cover that too

Where we left off¶

Last time, we made a class for representing measurements in feet-inches.

We just implemented the special __add__() method, which will make it work with the + operator.

In [4]:
class FeetInches:
    
    def __init__(self,f,i):
        self.feet = f
        self.inches = i
        
    def simplify(self):
        """
        if the number of inches is > 12, 
        this regroups the excess into feet
        """
        self.feet += self.inches // 12
        self.inches = self.inches % 12
    
    def __repr__(self):
        return str(self.feet)+"ft. "+str(self.inches)+"in."
    
    def __add__(self,other_measurement):
        total_feet = self.feet + other_measurement.feet
        total_inches = self.inches + other_measurement.inches
        
        #create an new FeetInches object with the new measurements
        total_FI = FeetInches(total_feet,total_inches)
        total_FI.simplify()
        return total_FI
    

measurement1 = FeetInches(3,6)
measurement2 = FeetInches(2,6)
total = measurement1 + measurement2
print(total)
6ft. 0in.

Similarly, you can support any of the following operators (plus more that I haven't listed):

  • +: object.__add__(self, other)
  • -: object.__sub__(self, other)
  • *: object.__mul__(self, other)
  • /: object.__truediv__(self, other)
  • //: object.__floordiv__(self, other)
  • %: object.__mod__(self, other)
  • **: object.__pow__(self, other)
  • <<: object.__lshift__(self, other)
  • >>: object.__rshift__(self, other)
  • &: object.__and__(self, other) (this is not the logical and operator)
  • ^: object.__xor__(self, other)
  • |: object.__or__(self, other) (this is not the logical or operator)
  • <: object.__lt__(self, other)
  • <=: object.__le__(self, other)
  • ==: object.__eq__(self, other)
  • !=: object.__ne__(self, other)
  • >: object.__gt__(self, other)
  • >=: object.__ge__(self, other)

Group Activity Problem 1¶

Add support for the subtraction operator to the FeetInches class.

Group Activity Problem 2¶

Discussion the following questions.

  • What is the name of the function would I have to define (i.e., like __add__ is for +) to be able to compare two FeetInches objects like in the code below?
  • Note that __add__ and __sub__ both return a new object of type FeetInches. What type of value should the function for < return?
In [ ]:
measurement1 = FeetInches(3,6)
measurement2 = FeetInches(2,6)
print(measurement1 < measurement2)

Group Activity Problem 3¶

Add support for the < (less than) operator to the FeetInches class.

Group Activity Problem 4¶

After you implement <, you might get some other operators like > for free. Try the code below and see if it works with your definition. Do <=, ==, !=, >= work? Why or why not?

In [ ]:
measurement1 = FeetInches(3,6)
measurement2 = FeetInches(2,6)
print(measurement1 > measurement2)

We'll use this space to go over the solution to the group activities above.

In [ ]:
class FeetInches:
    
    def __init__(self,f,i):
        self.feet = f
        self.inches = i
        
    def simplify(self):
        """
        if the number of inches is > 12, 
        this regroups the excess into feet
        """
        self.feet += self.inches // 12
        self.inches = self.inches % 12
    
    def __repr__(self):
        return str(self.feet)+"ft. "+str(self.inches)+"in."
    
    def __add__(self,other_measurement):
        total_feet = self.feet + other_measurement.feet
        total_inches = self.inches + other_measurement.inches
        
        #create an new FeetInches object with the new measurements
        total_FI = FeetInches(total_feet,total_inches)
        total_FI.simplify()
        return total_FI
    

measurement1 = FeetInches(3,6)
measurement2 = FeetInches(2,6)
total = measurement1 + measurement2
print(total)

A Playing Card Class¶

Let's say we want to create a class for representing playing cards (so we can implement games like War, Bridge, Black Jack, etc.).

Here is a bare-bones implementation:

In [1]:
class PlayingCard:
    
    def __init__(self,v,s):
        self.value = v
        self.suit = s
        
two_of_clubs = PlayingCard(2,"♣")
two_of_hearts = PlayingCard(2,"♡")
ten_of_hearts = PlayingCard(10,"♡")
seven_of_spades = PlayingCard(7,"♠")
four_of_diamonds = PlayingCard(4,"♢")

print("Here's what the card looks like:",ten_of_hearts)
Here's what the card looks like: <__main__.PlayingCard object at 0x107713370>

Note that ♣, ♡, ♠, and ♢ are all just text characters (see https://en.wikipedia.org/wiki/Playing_cards_in_Unicode ). You can use C, H, S, and D if you prefer.

Group Activity Problem 5¶

How should we handle cards without a numic value like Queens or Aces?

What do we have to do to get the cards to print nicely?

What do we have to do to allow us to write code like this?

Copy the code above and begin adding support for these.

In [ ]:
if two_of_clubs < ten_of_hearts:
    print("Player 2 wins the hand")
In [ ]:
 

Controlling Access¶

When designing a class, you should plan for how to protect against accidental misuse.

For example, what happens if someone tries to do this?

In [ ]:
pikachu = PlayingCard("Pikachu",40)

This particular class is only meant to represent standard French playing cards, so this should cause some kind of error.

One way to handle it might be like this:

In [2]:
class PlayingCard:
    
    def __init__(self,v,s):
        
        if type(v) != type(1) or v > 14 or v < 2:
            raise Exception("A PlayingCard's value must be an integer in the range 2-14.")
        self.value = v
        self.suit = s
        
twentyseven_of_clubs = PlayingCard(27,"♣")
pikachu = PlayingCard("Pikachu",40)
---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
Input In [2], in <cell line: 10>()
      7         self.value = v
      8         self.suit = s
---> 10 twentyseven_of_clubs = PlayingCard(27,"♣")
     11 pikachu = PlayingCard("Pikachu",40)

Input In [2], in PlayingCard.__init__(self, v, s)
      3 def __init__(self,v,s):
      5     if type(v) != type(1) or v > 14 or v < 2:
----> 6         raise Exception("A PlayingCard's value must be an integer in the range 2-14.")
      7     self.value = v
      8     self.suit = s

Exception: A PlayingCard's value must be an integer in the range 2-14.

Hiding Attributes¶

By convention, any attribute or method whose name starts with an underscore should be treated as private, meaning you shouldn't change the variable outside the class.

self._value

If it doesn't start with an underscore, it is public and changing it outside the class is fair game.

If you start it with two underscores, then Python performs name mangling, which doesn't let you change the name outside the class (unless you do something extra to get around the mangling).

self.__value

In [3]:
class PlayingCard:
    
    def __init__(self,v,s):
        
        if type(v) != type(1) or v > 14 or v < 2:
            raise Exception("A PlayingCard's value must be an integer in the range 2-14.")
        self.__value = v
        self.__suit = s
        
    def __repr__(self):
        return str(self.__value)+str(self.__suit)
        
pikachu = PlayingCard(2,"♣")
pikachu.__value = "Pikachu"  #this doesn't actually change self.__value
pikachu.__suit = 40
print(pikachu)
2♣

Group Activity Problem 6¶

Update your class so that self.value and self.suit are hidden (private/mangled).