Classes and Objects (2)

In today’s lecture, we will continue our discussion of object oriented programming in Python.

class Book:
    """This class represents a book with attributes title, author, and year"""
    
    # attributes
    # _ indicates that they are protected
    __slots__ = ['_title', '_author', '_year']  
    
    # __init__ is automatically called when we create new Book objects
    # we set the intial values of our attributes in __init__
    def __init__(self, bookTitle, bookAuthor, bookYear):
        self._title = bookTitle
        self._author = bookAuthor
        self._year = bookYear
    
    # accessor (getter) methods
    def getTitle(self):
        return self._title

    def getAuthor(self):
        return self._author
    
    def getYear(self):
        return self._year
    
    # mutator (setter) methods
    def setTitle(self, bookTitle):
        self._title = bookTitle
    
    def setAuthor(self, bookAuthor):
        self._author = bookAuthor
    
    def setYear(self, bookYear):
        self._year = int(bookYear)
    
    # methods for manipulating Books
    def numWordsInTitle(self):
        """Returns the number of words in title of book"""
        return len(self._title.split())
    
    def sameAuthorAs(self, otherBook):
        """Check if self and otherBook have same author"""
        return self._author == otherBook.getAuthor()
    
    def yearsSincePub(self, currentYear):
        """Returns the number of years since book was published"""
        return currentYear - self._year
    
    # __str__ is used to generate a meaningful string representation for Book objects
    # __str__ is automatically called when we ask to print() a Book object
    def __str__(self):
        return "'{}', by {}, in {}".format(self._title, self._author, self._year)
# creating book objects:
pp = Book('Pride and Prejudice', 'Jane Austen', 1813)
emma = Book('Emma', 'Jane Austen', 1815)
hp = Book("Harry Potter and the Sorcerer's Stone", "J.K. Rowling", 1997)
# we can access (non-private) attributes directly using dot notation
# (but this is not the preferred way!)
hp._title
"Harry Potter and the Sorcerer's Stone"
# invoke the accessor method (preferred way to access attributes)
pp.getTitle()
'Pride and Prejudice'
emma.getAuthor()
'Jane Austen'
hp.getYear()
1997
# invoke the mutator methods
hp.setYear(1997)
# verify that our update to year attribute in the previous line worked
hp.getYear()
1997
# invoke Book methods on specify books
hp.numWordsInTitle()
6
emma.yearsSincePub(2022)
207
hp.yearsSincePub(2022)
25
hp.sameAuthorAs(emma)
False
emma.sameAuthorAs(pp)
True
# test our __str__ method bring printing a specific Book instance
print(hp)
'Harry Potter and the Sorcerer's Stone', by J.K. Rowling, in 1997

In many instances, it is useful to specify default values for parameters. Python supports this in both method and function definitions. In this example, we specify default values for the parameters in init.

class Book2:
    """This class represents a book with attributes title, author, and year"""
    
    # attributes
    __slots__ = ['_title', '_author', '_year']  
    
    # this __init__ method specifies default values for the parameters
    def __init__(self, bookTitle="", bookAuthor="", bookYear=0):
        self._title = bookTitle
        self._author = bookAuthor
        self._year = bookYear

    def getTitle(self):
        return self._title

    def getAuthor(self):
        return self._author
    
    def getYear(self):
        return self._year
emptyBook = Book2()
emptyBook._title
''
emptyBook.getTitle()
''

Data Hiding via Attribute Types

When we create instance variables of a class, we must decide what level of access “users” of the class should have over the data and procedural attributes. Some OOP languages enforce these distinctions, Python uses a special naming convention to “signal the attribute type.”

  • Private (prefixed with __ or _): these attributes should only be used inside of the class definition. Among these, attributes with __ are strictly private and essentially invisible from outside. Attributes with _ can be used from outside but really should not be.

  • Public (no underscore prefix): these attributes can and should be freely used.

class TestingAttributes():
    __slots__ = ['__val', '_val', 'val']
    def __init__(self):
        self.__val = "I am strictly private."
        self._val = "I am private but accessible from outside."
        self.val = "I am public."
a = TestingAttributes()
a.val
'I am public.'
a._val
'I am private but accessible from outside.'
a.__val
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
/var/folders/md/kwd9nc_d2ns0hw9wsvdrnt2c0000gn/T/ipykernel_55943/133428337.py in <module>
----> 1 a.__val

AttributeError: 'TestingAttributes' object has no attribute '__val'

Another Example: Name class

In this example, we create a Name class that represents names, including a first, middle, and last name. This scenario illustrates a good reason to specify default parameter values in __init__. Also, note that we do not define mutator methods in this case since a person’s name cannot change (usually). Finally, we can choose how these names are printed in __str__.

class Name:
    """Class to represent a person's name."""
    __slots__ = ['_f', '_m', '_l']
 
    # since middle names are optional, we can define a default value
    def __init__(self, first, last, middle=''):
        self._f = first
        self._m = middle
        self._l = last

    # accessor methods for attributes
    def getFirst(self):
        return self._f
    
    def getMiddle(self):
        return self._m

    def getLast(self):
        return self._l

    def __str__(self):
        # if the person has a middle name
        if len(self.getMiddle()):  
            return '{}. {}. {}'.format(self.getFirst()[0], self.getMiddle()[0], self.getLast()) 
        else:
            return '{}. {}'.format(self.getFirst()[0], self.getLast())
n1 = Name('Rohit', 'Bhattacharya')
n2 = Name('Jeannie', 'Albrecht', 'Raye')
print(n1)
print(n2)
R. Bhattacharya
J. R. Albrecht

Notice Even though we can print the object now, if you ask about the name object in interactive python, it still gives something that is not human readable.

n1
<__main__.Name at 0x1097f9ec0>
type(n2)
__main__.Name

Summary

Today we saw how Python supports data abstraction (separating the data and details of the implementation from the user) via :

  • Data hiding: via attribute naming conventions

  • Encapsulation: bundling together of data and methods that provide an interface to the data (accessor and mutator methods)